diff --git a/README.md b/README.md index 61b803c4c..c88c49287 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,19 @@ import {GoogleGenAI} from '@google/genai'; const ai = new GoogleGenAI(); ``` +### Mutual TLS (mTLS) Authentication + +For Node.js environments requiring mTLS authentication, you can configure client certificates using environment variables: + +**Setting up mTLS with environment variables:** + +```bash +export GOOGLE_CLIENT_CERT='/path/to/client-cert.pem' +export GOOGLE_CLIENT_KEY='/path/to/client-key.pem' +``` + +Alternatively, you can use `GEMINI_CLIENT_CERT` and `GEMINI_CLIENT_KEY`. + ## API Selection By default, the SDK uses the beta API endpoints provided by Google to support diff --git a/api-report/genai-node.api.md b/api-report/genai-node.api.md index f36ca11a8..2d537c7c2 100644 --- a/api-report/genai-node.api.md +++ b/api-report/genai-node.api.md @@ -1801,6 +1801,7 @@ export enum HttpElementLocation { export interface HttpOptions { apiVersion?: string; baseUrl?: string; + dispatcher?: unknown; extraBody?: Record; headers?: Record; timeout?: number; diff --git a/api-report/genai-web.api.md b/api-report/genai-web.api.md index f36ca11a8..2d537c7c2 100644 --- a/api-report/genai-web.api.md +++ b/api-report/genai-web.api.md @@ -1801,6 +1801,7 @@ export enum HttpElementLocation { export interface HttpOptions { apiVersion?: string; baseUrl?: string; + dispatcher?: unknown; extraBody?: Record; headers?: Record; timeout?: number; diff --git a/api-report/genai.api.md b/api-report/genai.api.md index f36ca11a8..2d537c7c2 100644 --- a/api-report/genai.api.md +++ b/api-report/genai.api.md @@ -1801,6 +1801,7 @@ export enum HttpElementLocation { export interface HttpOptions { apiVersion?: string; baseUrl?: string; + dispatcher?: unknown; extraBody?: Record; headers?: Record; timeout?: number; diff --git a/package.json b/package.json index 7482aa3bb..dfee06bd8 100644 --- a/package.json +++ b/package.json @@ -108,13 +108,13 @@ "typedoc": "^0.27.0", "typescript": "~5.2.0", "typescript-eslint": "8.24.1", - "undici": "^7.16.0", "undici-types": "^7.16.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.4" }, "dependencies": { "google-auth-library": "^10.3.0", + "undici": "^7.16.0", "ws": "^8.18.0" }, "peerDependencies": { diff --git a/src/_api_client.ts b/src/_api_client.ts index 9a33e0b4c..2d84a4ef0 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -382,13 +382,17 @@ export class ApiClient { baseHttpOptions: types.HttpOptions, requestHttpOptions: types.HttpOptions, ): types.HttpOptions { - const patchedHttpOptions = JSON.parse( - JSON.stringify(baseHttpOptions), - ) as types.HttpOptions; + // Shallow clone to preserve non-serializable fields like dispatcher + const patchedHttpOptions: types.HttpOptions = {...baseHttpOptions}; for (const [key, value] of Object.entries(requestHttpOptions)) { + // Skip dispatcher if it's being patched from baseHttpOptions + if (key === 'dispatcher') { + patchedHttpOptions[key] = value; + continue; + } // Records compile to objects. - if (typeof value === 'object') { + if (typeof value === 'object' && value !== null) { // @ts-expect-error TS2345TS7053: Element implicitly has an 'any' type // because expression of type 'string' can't be used to index type // 'HttpOptions'. @@ -471,6 +475,11 @@ export class ApiClient { httpOptions.extraBody as Record, ); } + if (httpOptions && httpOptions.dispatcher) { + // @ts-expect-error TS2339: Property 'dispatcher' does not exist on type 'RequestInit'. + // This is a Node.js-specific property for undici + requestInit.dispatcher = httpOptions.dispatcher; + } requestInit.headers = await this.getHeadersInternal(httpOptions, url); return requestInit; } diff --git a/src/node/node_client.ts b/src/node/node_client.ts index 17bd0e256..d97b690a8 100644 --- a/src/node/node_client.ts +++ b/src/node/node_client.ts @@ -4,7 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as fs from 'fs'; import {GoogleAuthOptions} from 'google-auth-library'; +import {Agent} from 'undici'; import {ApiClient} from '../_api_client.js'; import {getBaseUrl} from '../_base_url.js'; @@ -158,6 +160,23 @@ export class GoogleGenAI { } } + // Configure mTLS if client certificates are provided + const clientCert = getClientCertFromEnv(); + const clientKey = getClientKeyFromEnv(); + if (clientCert && clientKey) { + const agent = new Agent({ + connect: { + cert: clientCert, + key: clientKey, + }, + }); + if (options.httpOptions) { + options.httpOptions.dispatcher = agent; + } else { + options.httpOptions = {dispatcher: agent}; + } + } + this.apiVersion = options.apiVersion; this.httpOptions = options.httpOptions; const auth = new NodeAuth({ @@ -214,3 +233,43 @@ function getApiKeyFromEnv(): string | undefined { } return envGoogleApiKey || envGeminiApiKey || undefined; } + +function getClientCertFromEnv(): string | undefined { + const envGoogleClientCert = getEnv('GOOGLE_CLIENT_CERT'); + const envGeminiClientCert = getEnv('GEMINI_CLIENT_CERT'); + if (envGoogleClientCert && envGeminiClientCert) { + console.warn( + 'Both GOOGLE_CLIENT_CERT and GEMINI_CLIENT_CERT are set. Using GOOGLE_CLIENT_CERT.', + ); + } + const certPath = envGoogleClientCert || envGeminiClientCert; + if (certPath) { + try { + return fs.readFileSync(certPath, 'utf8'); + } catch (error) { + throw new Error( + `Failed to read client certificate from ${certPath}: ${error}`, + ); + } + } + return undefined; +} + +function getClientKeyFromEnv(): string | undefined { + const envGoogleClientKey = getEnv('GOOGLE_CLIENT_KEY'); + const envGeminiClientKey = getEnv('GEMINI_CLIENT_KEY'); + if (envGoogleClientKey && envGeminiClientKey) { + console.warn( + 'Both GOOGLE_CLIENT_KEY and GEMINI_CLIENT_KEY are set. Using GOOGLE_CLIENT_KEY.', + ); + } + const keyPath = envGoogleClientKey || envGeminiClientKey; + if (keyPath) { + try { + return fs.readFileSync(keyPath, 'utf8'); + } catch (error) { + throw new Error(`Failed to read client key from ${keyPath}: ${error}`); + } + } + return undefined; +} diff --git a/src/types.ts b/src/types.ts index 3f48de4b3..4b089aaae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1572,6 +1572,9 @@ export declare interface HttpOptions { - VertexAI backend API docs: https://cloud.google.com/vertex-ai/docs/reference/rest - GeminiAPI backend API docs: https://ai.google.dev/api/rest */ extraBody?: Record; + /** Undici dispatcher for custom HTTP agent configuration (Node.js only). + This can be used to configure mTLS with client certificates. */ + dispatcher?: unknown; } /** Schema is used to define the format of input/output data.