Skip to content

Commit cbf333a

Browse files
feat: add mTLS client certificate support
1 parent a345449 commit cbf333a

5 files changed

Lines changed: 72 additions & 16 deletions

File tree

package-lock.json

Lines changed: 1 addition & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client-common.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ export class SpiceClient {
423423
private _httpUrl: string;
424424
private _userAgent: string;
425425
private _flightTlsEnabled: boolean = true;
426+
private _tlsClientCertFile?: string;
427+
private _tlsClientKeyFile?: string;
428+
private _tlsRootCertFile?: string;
426429
private _maxRetries: number;
427430
private _customHeaders?: { [key: string]: string };
428431
private _platform: PlatformAdapter;
@@ -466,6 +469,9 @@ export class SpiceClient {
466469
flightOnly,
467470
httpOnly,
468471
logging,
472+
tlsClientCertFile,
473+
tlsClientKeyFile,
474+
tlsRootCertFile,
469475
} = params;
470476

471477
// Initialize logger (default: enabled)
@@ -507,6 +513,9 @@ export class SpiceClient {
507513
? `${userAgent} ${platform.getUserAgent()}`
508514
: platform.getUserAgent();
509515
this._customHeaders = customHeaders;
516+
this._tlsClientCertFile = tlsClientCertFile;
517+
this._tlsClientKeyFile = tlsClientKeyFile;
518+
this._tlsRootCertFile = tlsRootCertFile;
510519
}
511520

512521
// Determine if this is Spice Cloud endpoint (compute once)
@@ -526,6 +535,9 @@ export class SpiceClient {
526535
this._userAgent,
527536
this._flightTlsEnabled,
528537
this._logger,
538+
this._tlsClientCertFile,
539+
this._tlsClientKeyFile,
540+
this._tlsRootCertFile,
529541
);
530542
}
531543

src/client.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class SpiceClient {
168168
private _useGrpc: boolean = grpcAvailable;
169169
private _initPromise: Promise<void>;
170170
private _customHeaders?: { [key: string]: string };
171+
private _httpsAgent: https.Agent;
171172

172173
public constructor(params: string | SpiceClientConfig = {}) {
173174
// support legacy constructor with api_key as first agument
@@ -176,6 +177,7 @@ class SpiceClient {
176177
this._httpUrl = 'https://data.spiceai.io';
177178
this._flightUrl = 'flight.spiceai.io:443';
178179
this._userAgent = getUserAgent();
180+
this._httpsAgent = new https.Agent({ keepAlive: true });
179181
} else {
180182
const {
181183
apiKey,
@@ -184,6 +186,9 @@ class SpiceClient {
184186
flightTlsEnabled,
185187
userAgent,
186188
customHeaders,
189+
tlsClientCertFile,
190+
tlsClientKeyFile,
191+
tlsRootCertFile,
187192
} = params;
188193

189194
this._apiKey = apiKey;
@@ -204,6 +209,30 @@ class SpiceClient {
204209
? `${userAgent} ${getUserAgent()}`
205210
: getUserAgent();
206211
this._customHeaders = customHeaders;
212+
213+
// Validate that client cert and key are either both set or both unset
214+
if (
215+
(tlsClientCertFile && !tlsClientKeyFile) ||
216+
(!tlsClientCertFile && tlsClientKeyFile)
217+
) {
218+
const missing = tlsClientCertFile
219+
? 'tlsClientKeyFile'
220+
: 'tlsClientCertFile';
221+
throw new Error(
222+
`Both tlsClientCertFile and tlsClientKeyFile must be provided together for mTLS. ${missing} is missing.`,
223+
);
224+
}
225+
226+
// Build per-instance HTTPS agent with optional mTLS certs
227+
const agentOpts: https.AgentOptions = { keepAlive: true };
228+
if (tlsRootCertFile) {
229+
agentOpts.ca = fs.readFileSync(tlsRootCertFile);
230+
}
231+
if (tlsClientCertFile && tlsClientKeyFile) {
232+
agentOpts.cert = fs.readFileSync(tlsClientCertFile);
233+
agentOpts.key = fs.readFileSync(tlsClientKeyFile);
234+
}
235+
this._httpsAgent = new https.Agent(agentOpts);
207236
}
208237

209238
// Initialize gRPC during construction
@@ -822,7 +851,7 @@ class SpiceClient {
822851
};
823852

824853
if (this._httpUrl.startsWith('https://')) {
825-
fetchOptions.agent = httpsAgent;
854+
fetchOptions.agent = this._httpsAgent;
826855
}
827856

828857
return fetch(url, fetchOptions);

src/grpc/client.node.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,19 +140,29 @@ export class GrpcFlightClient {
140140
private initPromise: Promise<void>;
141141
private useGrpc: boolean = grpcAvailable;
142142
private logger: Logger;
143+
private tlsClientCertFile?: string;
144+
private tlsClientKeyFile?: string;
145+
146+
private tlsRootCertFile?: string;
143147

144148
constructor(
145149
apiKey: string | undefined,
146150
flightUrl: string,
147151
userAgent: string,
148152
flightTlsEnabled: boolean,
149153
logger?: Logger,
154+
tlsClientCertFile?: string,
155+
tlsClientKeyFile?: string,
156+
tlsRootCertFile?: string,
150157
) {
151158
this.apiKey = apiKey;
152159
this.flightUrl = flightUrl;
153160
this.userAgent = userAgent;
154161
this.flightTlsEnabled = flightTlsEnabled;
155162
this.logger = logger || new Logger(true);
163+
this.tlsClientCertFile = tlsClientCertFile;
164+
this.tlsClientKeyFile = tlsClientKeyFile;
165+
this.tlsRootCertFile = tlsRootCertFile;
156166
this.initPromise = this.initialize();
157167
}
158168

@@ -219,7 +229,10 @@ export class GrpcFlightClient {
219229
);
220230
}
221231

222-
const creds = grpc.credentials.createSsl();
232+
const rootCerts = this.tlsRootCertFile ? fs.readFileSync(this.tlsRootCertFile) : null;
233+
const clientCert = this.tlsClientCertFile ? fs.readFileSync(this.tlsClientCertFile) : null;
234+
const clientKey = this.tlsClientKeyFile ? fs.readFileSync(this.tlsClientKeyFile) : null;
235+
const creds = grpc.credentials.createSsl(rootCerts, clientKey, clientCert);
223236
const metaCallback = (_params: any, callback: any) => {
224237
callback(null, meta);
225238
};

src/interfaces.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ export interface SpiceClientConfig {
2323
* @default true
2424
*/
2525
logging?: boolean;
26+
/**
27+
* Path to a PEM-encoded CA certificate file for server verification.
28+
* When set, this CA is used instead of the system certificate store.
29+
*/
30+
tlsRootCertFile?: string;
31+
/**
32+
* Path to a PEM-encoded client certificate file for mTLS.
33+
* Must be used together with `tlsClientKeyFile`.
34+
*/
35+
tlsClientCertFile?: string;
36+
/**
37+
* Path to a PEM-encoded client private key file for mTLS.
38+
* Must be used together with `tlsClientCertFile`.
39+
*/
40+
tlsClientKeyFile?: string;
2641
}
2742

2843
export interface SchemaField {

0 commit comments

Comments
 (0)