Skip to content

Commit 202ebab

Browse files
authored
refactor: move transport logic to a ToolboxTransport class (#188)
* refactor: move transport logic to a ToolboxTransport class * rename file * update license * basic lint * lint * log errors properly * fix error handling * add test cov * lint * small refactor * fix consoleErrorSpy issue
1 parent ca25074 commit 202ebab

File tree

8 files changed

+757
-992
lines changed

8 files changed

+757
-992
lines changed

packages/toolbox-core/src/toolbox_core/client.ts

Lines changed: 14 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,14 @@
1313
// limitations under the License.
1414

1515
import {ToolboxTool} from './tool.js';
16-
import axios from 'axios';
16+
import {AxiosInstance} from 'axios';
17+
import {ITransport} from './transport.types.js';
18+
import {ToolboxTransport} from './toolboxTransport.js';
1719
import {
18-
type AxiosInstance,
19-
type AxiosRequestConfig,
20-
type AxiosResponse,
21-
} from 'axios';
22-
import {
23-
ZodManifestSchema,
2420
createZodSchemaFromParams,
2521
ParameterSchema,
22+
ZodManifestSchema,
2623
} from './protocol.js';
27-
import {logApiError} from './errorUtils.js';
28-
import {ZodError} from 'zod';
2924
import {BoundParams, identifyAuthRequirements, resolveValue} from './utils.js';
3025
import {AuthTokenGetters, RequiredAuthnParams} from './tool.js';
3126

@@ -41,8 +36,7 @@ export type ClientHeadersConfig = Record<string, ClientHeaderProvider>;
4136
* An asynchronous client for interacting with a Toolbox service.
4237
*/
4338
class ToolboxClient {
44-
#baseUrl: string;
45-
#session: AxiosInstance;
39+
#transport: ITransport;
4640
#clientHeaders: ClientHeadersConfig;
4741

4842
/**
@@ -58,8 +52,7 @@ class ToolboxClient {
5852
session?: AxiosInstance | null,
5953
clientHeaders?: ClientHeadersConfig | null,
6054
) {
61-
this.#baseUrl = url;
62-
this.#session = session || axios.create({baseURL: this.#baseUrl});
55+
this.#transport = new ToolboxTransport(url, session || undefined);
6356
this.#clientHeaders = clientHeaders || {};
6457
}
6558

@@ -77,47 +70,6 @@ class ToolboxClient {
7770
return Object.fromEntries(resolvedEntries);
7871
}
7972

80-
/**
81-
* Fetches and parses the manifest from a given API path.
82-
* @param {string} apiPath - The API path to fetch the manifest from (e.g., "/api/tool/mytool").
83-
* @returns {Promise<Manifest>} A promise that resolves to the parsed manifest.
84-
* @throws {Error} If there's an error fetching data or if the manifest structure is invalid.
85-
*/
86-
async #fetchAndParseManifest(apiPath: string): Promise<Manifest> {
87-
const url = `${this.#baseUrl}${apiPath}`;
88-
try {
89-
const headers = await this.#resolveClientHeaders();
90-
const config: AxiosRequestConfig = {headers};
91-
const response: AxiosResponse = await this.#session.get(url, config);
92-
const responseData = response.data;
93-
94-
try {
95-
const manifest = ZodManifestSchema.parse(responseData);
96-
return manifest;
97-
} catch (validationError) {
98-
let detailedMessage = `Invalid manifest structure received from ${url}: `;
99-
if (validationError instanceof ZodError) {
100-
const issueDetails = validationError.issues;
101-
detailedMessage += JSON.stringify(issueDetails, null, 2);
102-
} else if (validationError instanceof Error) {
103-
detailedMessage += validationError.message;
104-
} else {
105-
detailedMessage += 'Unknown validation error.';
106-
}
107-
throw new Error(detailedMessage);
108-
}
109-
} catch (error) {
110-
if (
111-
error instanceof Error &&
112-
error.message.startsWith('Invalid manifest structure received from')
113-
) {
114-
throw error;
115-
}
116-
logApiError(`Error fetching data from ${url}:`, error);
117-
throw error;
118-
}
119-
}
120-
12173
/**
12274
* Creates a ToolboxTool instance from its schema.
12375
* @param {string} toolName - The name of the tool.
@@ -159,8 +111,7 @@ class ToolboxClient {
159111
const paramZodSchema = createZodSchemaFromParams(params);
160112

161113
const tool = ToolboxTool(
162-
this.#session,
163-
this.#baseUrl,
114+
this.#transport,
164115
toolName,
165116
toolSchema.description,
166117
paramZodSchema,
@@ -195,8 +146,8 @@ class ToolboxClient {
195146
authTokenGetters: AuthTokenGetters | null = {},
196147
boundParams: BoundParams | null = {},
197148
): Promise<ReturnType<typeof ToolboxTool>> {
198-
const apiPath = `/api/tool/${name}`;
199-
const manifest = await this.#fetchAndParseManifest(apiPath);
149+
const headers = await this.#resolveClientHeaders();
150+
const manifest = await this.#transport.toolGet(name, headers);
200151

201152
if (
202153
manifest.tools &&
@@ -240,7 +191,9 @@ class ToolboxClient {
240191
}
241192
return tool;
242193
} else {
243-
throw new Error(`Tool "${name}" not found in manifest from ${apiPath}.`);
194+
throw new Error(
195+
`Tool "${name}" not found in manifest from ${this.#transport.baseUrl}/api/tool/${name}.`,
196+
);
244197
}
245198
}
246199

@@ -262,9 +215,9 @@ class ToolboxClient {
262215
strict = false,
263216
): Promise<Array<ReturnType<typeof ToolboxTool>>> {
264217
const toolsetName = name || '';
265-
const apiPath = `/api/toolset/${toolsetName}`;
218+
const headers = await this.#resolveClientHeaders();
266219

267-
const manifest = await this.#fetchAndParseManifest(apiPath);
220+
const manifest = await this.#transport.toolsList(toolsetName, headers);
268221
const tools: Array<ReturnType<typeof ToolboxTool>> = [];
269222

270223
const overallUsedAuthKeys: Set<string> = new Set();

packages/toolbox-core/src/toolbox_core/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
export {ToolboxClient} from './client.js';
1818

19+
// Export transport classes
20+
export {ToolboxTransport} from './toolboxTransport.js';
21+
1922
// Export the main factory function and the core tool type
2023
export {ToolboxTool} from './tool.js';
2124

packages/toolbox-core/src/toolbox_core/tool.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
// limitations under the License.
1414

1515
import {ZodObject, ZodError, ZodRawShape} from 'zod';
16-
import {AxiosInstance, AxiosResponse} from 'axios';
1716

18-
import {logApiError} from './errorUtils.js';
17+
import {ITransport} from './transport.types.js';
1918
import {
2019
BoundParams,
2120
BoundValue,
@@ -41,8 +40,7 @@ function getAuthHeaderName(authTokenName: string): string {
4140
* Creates a callable tool function representing a specific tool on a remote
4241
* Toolbox server.
4342
*
44-
* @param {AxiosInstance} session - The Axios session for making HTTP requests.
45-
* @param {string} baseUrl - The base URL of the Toolbox Server API.
43+
* @param {ITransport} transport - The transport for making API requests.
4644
* @param {string} name - The name of the remote tool.
4745
* @param {string} description - A description of the remote tool.
4846
* @param {ZodObject<any>} paramSchema - The Zod schema for validating the tool's parameters.
@@ -55,8 +53,7 @@ function getAuthHeaderName(authTokenName: string): string {
5553
* called, invokes the tool with the provided arguments.
5654
*/
5755
function ToolboxTool(
58-
session: AxiosInstance,
59-
baseUrl: string,
56+
transport: ITransport,
6057
name: string,
6158
description: string,
6259
paramSchema: ZodObject<ZodRawShape>,
@@ -69,14 +66,13 @@ function ToolboxTool(
6966
if (
7067
(Object.keys(authTokenGetters).length > 0 ||
7168
Object.keys(clientHeaders).length > 0) &&
72-
!baseUrl.startsWith('https://')
69+
!transport.baseUrl.startsWith('https://')
7370
) {
7471
console.warn(
7572
'Sending ID token over HTTP. User data may be exposed. Use HTTPS for secure communication.',
7673
);
7774
}
7875

79-
const toolUrl = `${baseUrl}/api/tool/${name}/invoke`;
8076
const boundKeys = Object.keys(boundParams);
8177
const userParamSchema = paramSchema.omit(
8278
Object.fromEntries(boundKeys.map(k => [k, true])),
@@ -159,19 +155,7 @@ function ToolboxTool(
159155
headers[getAuthHeaderName(authService)] = token;
160156
}
161157

162-
try {
163-
const response: AxiosResponse = await session.post(
164-
toolUrl,
165-
filteredPayload,
166-
{
167-
headers,
168-
},
169-
);
170-
return response.data.result;
171-
} catch (error) {
172-
logApiError(`Error posting data to ${toolUrl}:`, error);
173-
throw error;
174-
}
158+
return await transport.toolInvoke(name, filteredPayload, headers);
175159
};
176160
callable.toolName = name;
177161
callable.description = description;
@@ -234,8 +218,7 @@ function ToolboxTool(
234218
}
235219

236220
return ToolboxTool(
237-
session,
238-
baseUrl,
221+
transport,
239222
this.toolName,
240223
this.description,
241224
this.params,
@@ -271,8 +254,7 @@ function ToolboxTool(
271254

272255
const newBoundParams = {...this.boundParams, ...paramsToBind};
273256
return ToolboxTool(
274-
session,
275-
baseUrl,
257+
transport,
276258
this.toolName,
277259
this.description,
278260
this.params,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import axios, {AxiosInstance, AxiosResponse} from 'axios';
18+
import {ITransport} from './transport.types.js';
19+
import {ZodManifest, ZodManifestSchema} from './protocol.js';
20+
import {logApiError} from './errorUtils.js';
21+
22+
/**
23+
* Transport for the native Toolbox protocol.
24+
*/
25+
export class ToolboxTransport implements ITransport {
26+
readonly #baseUrl: string;
27+
#session: AxiosInstance;
28+
29+
constructor(baseUrl: string, session?: AxiosInstance) {
30+
this.#baseUrl = baseUrl;
31+
// If no axios session is provided, make our own
32+
this.#session = session || axios.create({baseURL: this.baseUrl});
33+
}
34+
35+
get baseUrl(): string {
36+
return this.#baseUrl;
37+
}
38+
39+
async #getManifest(
40+
url: string,
41+
headers?: Record<string, string>,
42+
): Promise<ZodManifest> {
43+
/** Helper method to perform GET requests and parse the ManifestSchema. */
44+
try {
45+
const response: AxiosResponse = await this.#session.get(url, {headers});
46+
return ZodManifestSchema.parse(response.data);
47+
} catch (error) {
48+
logApiError(`Error fetching data from ${url}:`, error);
49+
throw error;
50+
}
51+
}
52+
53+
async toolGet(
54+
toolName: string,
55+
headers?: Record<string, string>,
56+
): Promise<ZodManifest> {
57+
const url = `${this.#baseUrl}/api/tool/${toolName}`;
58+
return await this.#getManifest(url, headers);
59+
}
60+
61+
async toolsList(
62+
toolsetName?: string,
63+
headers?: Record<string, string>,
64+
): Promise<ZodManifest> {
65+
const url = `${this.#baseUrl}/api/toolset/${toolsetName || ''}`;
66+
return await this.#getManifest(url, headers);
67+
}
68+
69+
async toolInvoke(
70+
toolName: string,
71+
arguments_: Record<string, unknown>,
72+
headers: Record<string, string>,
73+
): Promise<string> {
74+
// ID tokens contain sensitive user information (claims). Transmitting
75+
// these over HTTP exposes the data to interception and unauthorized
76+
// access. Always use HTTPS to ensure secure communication and protect
77+
// user privacy.
78+
if (
79+
this.baseUrl.startsWith('http://') &&
80+
headers &&
81+
Object.keys(headers).length > 0
82+
) {
83+
console.warn(
84+
'Sending data token over HTTP. User data may be exposed. Use HTTPS for secure communication.',
85+
);
86+
}
87+
const url = `${this.#baseUrl}/api/tool/${toolName}/invoke`;
88+
try {
89+
const response: AxiosResponse = await this.#session.post(
90+
url,
91+
arguments_,
92+
{
93+
headers,
94+
},
95+
);
96+
const body = response.data;
97+
if (body?.error) {
98+
throw new Error(body.error);
99+
}
100+
return body.result;
101+
} catch (error) {
102+
logApiError(`Error posting data to ${url}:`, error);
103+
throw error;
104+
}
105+
}
106+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {ZodManifest} from './protocol.js';
18+
19+
/**
20+
* Defines the contract for a 'smart' transport that handles both
21+
* protocol formatting and network communication.
22+
*/
23+
export interface ITransport {
24+
/**
25+
* The base URL for the transport.
26+
*/
27+
readonly baseUrl: string;
28+
29+
/**
30+
* Gets a single tool from the server.
31+
*/
32+
toolGet(
33+
toolName: string,
34+
headers?: Record<string, string>,
35+
): Promise<ZodManifest>;
36+
37+
/**
38+
* Lists available tools from the server.
39+
*/
40+
toolsList(
41+
toolsetName?: string,
42+
headers?: Record<string, string>,
43+
): Promise<ZodManifest>;
44+
45+
/**
46+
* Invokes a specific tool on the server.
47+
*/
48+
toolInvoke(
49+
toolName: string,
50+
arguments_: Record<string, unknown>,
51+
headers: Record<string, string>,
52+
): Promise<string>;
53+
}

0 commit comments

Comments
 (0)