galileo-generated 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/dist/commonjs/hooks/cert-management.d.ts +73 -0
  3. package/dist/commonjs/hooks/cert-management.d.ts.map +1 -0
  4. package/dist/commonjs/hooks/cert-management.js +258 -0
  5. package/dist/commonjs/hooks/cert-management.js.map +1 -0
  6. package/dist/commonjs/hooks/registration.d.ts.map +1 -1
  7. package/dist/commonjs/hooks/registration.js +4 -0
  8. package/dist/commonjs/hooks/registration.js.map +1 -1
  9. package/dist/commonjs/lib/galileo-config.d.ts +101 -12
  10. package/dist/commonjs/lib/galileo-config.d.ts.map +1 -1
  11. package/dist/commonjs/lib/galileo-config.js +153 -12
  12. package/dist/commonjs/lib/galileo-config.js.map +1 -1
  13. package/dist/commonjs/tests/hooks/cert-management.test.d.ts +2 -0
  14. package/dist/commonjs/tests/hooks/cert-management.test.d.ts.map +1 -0
  15. package/dist/commonjs/tests/hooks/cert-management.test.js +794 -0
  16. package/dist/commonjs/tests/hooks/cert-management.test.js.map +1 -0
  17. package/dist/commonjs/tests/lib/galileo-config.test.js +101 -0
  18. package/dist/commonjs/tests/lib/galileo-config.test.js.map +1 -1
  19. package/dist/esm/hooks/cert-management.d.ts +73 -0
  20. package/dist/esm/hooks/cert-management.d.ts.map +1 -0
  21. package/dist/esm/hooks/cert-management.js +254 -0
  22. package/dist/esm/hooks/cert-management.js.map +1 -0
  23. package/dist/esm/hooks/registration.d.ts.map +1 -1
  24. package/dist/esm/hooks/registration.js +4 -0
  25. package/dist/esm/hooks/registration.js.map +1 -1
  26. package/dist/esm/lib/galileo-config.d.ts +101 -12
  27. package/dist/esm/lib/galileo-config.d.ts.map +1 -1
  28. package/dist/esm/lib/galileo-config.js +153 -12
  29. package/dist/esm/lib/galileo-config.js.map +1 -1
  30. package/dist/esm/tests/hooks/cert-management.test.d.ts +2 -0
  31. package/dist/esm/tests/hooks/cert-management.test.d.ts.map +1 -0
  32. package/dist/esm/tests/hooks/cert-management.test.js +792 -0
  33. package/dist/esm/tests/hooks/cert-management.test.js.map +1 -0
  34. package/dist/esm/tests/lib/galileo-config.test.js +101 -0
  35. package/dist/esm/tests/lib/galileo-config.test.js.map +1 -1
  36. package/package.json +5 -3
  37. package/src/hooks/cert-management.ts +288 -0
  38. package/src/hooks/registration.ts +5 -0
  39. package/src/lib/galileo-config.ts +214 -15
  40. package/src/tests/hooks/cert-management.test.ts +958 -0
  41. package/src/tests/lib/galileo-config.test.ts +110 -0
@@ -0,0 +1,792 @@
1
+ import { describe, test, expect, afterEach, beforeEach, vi } from 'vitest';
2
+ import { CertManagementHook } from '../../hooks/cert-management.js';
3
+ import { GalileoConfig } from '../../lib/galileo-config.js';
4
+ import { HTTPClient } from '../../lib/http.js';
5
+ import { writeFileSync, mkdirSync, rmSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+ // Mock runtime detection - default to Node.js environment
9
+ const { mockIsNodeLike, mockIsBrowserLike } = vi.hoisted(() => ({
10
+ mockIsNodeLike: vi.fn(() => true),
11
+ mockIsBrowserLike: vi.fn(() => false),
12
+ }));
13
+ vi.mock('../../lib/runtime.js', () => ({
14
+ isNodeLike: mockIsNodeLike,
15
+ isBrowserLike: mockIsBrowserLike,
16
+ isDeno: vi.fn(() => false),
17
+ }));
18
+ const ENV_KEYS = [
19
+ 'GALILEO_API_KEY',
20
+ 'GALILEO_CA_CERT_PATH',
21
+ 'SSL_CERT_FILE',
22
+ 'NODE_EXTRA_CA_CERTS',
23
+ 'GALILEO_CLIENT_CERT_PATH',
24
+ 'GALILEO_CLIENT_KEY_PATH',
25
+ 'GALILEO_REJECT_UNAUTHORIZED',
26
+ 'NODE_TLS_REJECT_UNAUTHORIZED',
27
+ ];
28
+ function clearEnv() {
29
+ for (const key of ENV_KEYS) {
30
+ delete process.env[key];
31
+ }
32
+ }
33
+ describe('CertManagementHook', () => {
34
+ let tmpDir;
35
+ let caCertPath;
36
+ let clientCertPath;
37
+ let clientKeyPath;
38
+ beforeEach(() => {
39
+ // Create temporary directory and test certificate files
40
+ tmpDir = join(tmpdir(), `galileo-cert-test-${Date.now()}`);
41
+ mkdirSync(tmpDir, { recursive: true });
42
+ caCertPath = join(tmpDir, 'ca.pem');
43
+ clientCertPath = join(tmpDir, 'client.pem');
44
+ clientKeyPath = join(tmpDir, 'key.pem');
45
+ writeFileSync(caCertPath, '-----BEGIN CERTIFICATE-----\nMOCK_CA_CERT\n-----END CERTIFICATE-----');
46
+ writeFileSync(clientCertPath, '-----BEGIN CERTIFICATE-----\nMOCK_CLIENT_CERT\n-----END CERTIFICATE-----');
47
+ writeFileSync(clientKeyPath, '-----BEGIN PRIVATE KEY-----\nMOCK_PRIVATE_KEY\n-----END PRIVATE KEY-----');
48
+ // Default to Node.js environment
49
+ mockIsNodeLike.mockReturnValue(true);
50
+ mockIsBrowserLike.mockReturnValue(false);
51
+ });
52
+ afterEach(() => {
53
+ vi.clearAllMocks();
54
+ clearEnv();
55
+ GalileoConfig.reset();
56
+ // Clean up temporary directory
57
+ if (tmpDir) {
58
+ rmSync(tmpDir, { recursive: true, force: true });
59
+ }
60
+ });
61
+ describe('sdkInit', () => {
62
+ test('test sdkInit returns httpClient when CA cert configured', () => {
63
+ GalileoConfig.reset();
64
+ GalileoConfig.get({
65
+ apiKey: 'test-key',
66
+ apiUrl: 'https://api.example.com',
67
+ caCertPath,
68
+ });
69
+ const hook = new CertManagementHook();
70
+ const opts = { serverURL: 'https://api.example.com' };
71
+ const result = hook.sdkInit(opts);
72
+ expect(result.httpClient).toBeDefined();
73
+ expect(result.httpClient).not.toBe(opts.httpClient);
74
+ });
75
+ test('test sdkInit returns httpClient when CA cert content configured', () => {
76
+ GalileoConfig.reset();
77
+ GalileoConfig.get({
78
+ apiKey: 'test-key',
79
+ apiUrl: 'https://api.example.com',
80
+ caCertContent: '-----BEGIN CERTIFICATE-----\nMOCK_CONTENT\n-----END CERTIFICATE-----',
81
+ });
82
+ const hook = new CertManagementHook();
83
+ const opts = { serverURL: 'https://api.example.com' };
84
+ const result = hook.sdkInit(opts);
85
+ expect(result.httpClient).toBeDefined();
86
+ });
87
+ test('test sdkInit returns httpClient with client certificates', () => {
88
+ GalileoConfig.reset();
89
+ GalileoConfig.get({
90
+ apiKey: 'test-key',
91
+ apiUrl: 'https://api.example.com',
92
+ caCertPath,
93
+ clientCertPath,
94
+ clientKeyPath,
95
+ });
96
+ const hook = new CertManagementHook();
97
+ const opts = { serverURL: 'https://api.example.com' };
98
+ const result = hook.sdkInit(opts);
99
+ expect(result.httpClient).toBeDefined();
100
+ });
101
+ test('test sdkInit returns original opts when no cert configured', () => {
102
+ GalileoConfig.reset();
103
+ GalileoConfig.get({
104
+ apiKey: 'test-key',
105
+ apiUrl: 'https://api.example.com',
106
+ });
107
+ const hook = new CertManagementHook();
108
+ const opts = { serverURL: 'https://api.example.com' };
109
+ const result = hook.sdkInit(opts);
110
+ expect(result).toBe(opts);
111
+ expect(result.httpClient).toBeUndefined();
112
+ });
113
+ test('test sdkInit augments existing httpClient instead of replacing it', async () => {
114
+ GalileoConfig.reset();
115
+ GalileoConfig.get({
116
+ apiKey: 'test-key',
117
+ apiUrl: 'https://api.example.com',
118
+ caCertPath,
119
+ });
120
+ const hook = new CertManagementHook();
121
+ const mockFetcher = vi.fn().mockResolvedValue(new Response());
122
+ const existingClient = new HTTPClient({ fetcher: mockFetcher });
123
+ const opts = {
124
+ serverURL: 'https://api.example.com',
125
+ httpClient: existingClient,
126
+ };
127
+ const result = hook.sdkInit(opts);
128
+ expect(result.serverURL).toBe(opts.serverURL);
129
+ expect(result.httpClient).toBeDefined();
130
+ // Most important: the same httpClient instance is returned, not a new one
131
+ expect(result.httpClient).toBe(existingClient);
132
+ // Verify that the TLS hook was added by making a request
133
+ const req = new Request('https://api.example.com/test', { method: 'GET' });
134
+ await result.httpClient?.request(req);
135
+ // The custom fetcher should have been called
136
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
137
+ const callArgs = mockFetcher.mock.calls[0];
138
+ expect(callArgs).toBeDefined();
139
+ if (!callArgs)
140
+ throw new Error('unreachable');
141
+ const [calledReq] = callArgs;
142
+ // Verify dispatcher was injected into the request
143
+ expect(calledReq).toBeInstanceOf(Request);
144
+ expect(calledReq.url).toBe('https://api.example.com/test');
145
+ });
146
+ test('test sdkInit skips cert loading in browser environment', () => {
147
+ mockIsNodeLike.mockReturnValue(false);
148
+ mockIsBrowserLike.mockReturnValue(true);
149
+ GalileoConfig.reset();
150
+ GalileoConfig.get({
151
+ apiKey: 'test-key',
152
+ apiUrl: 'https://api.example.com',
153
+ caCertPath,
154
+ });
155
+ const hook = new CertManagementHook();
156
+ const opts = { serverURL: 'https://api.example.com' };
157
+ const result = hook.sdkInit(opts);
158
+ expect(result).toBe(opts);
159
+ expect(result.httpClient).toBeUndefined();
160
+ });
161
+ test('test sdkInit returns original opts when CA cert file missing', () => {
162
+ GalileoConfig.reset();
163
+ GalileoConfig.get({
164
+ apiKey: 'test-key',
165
+ apiUrl: 'https://api.example.com',
166
+ caCertPath: '/nonexistent/ca.pem',
167
+ });
168
+ const hook = new CertManagementHook();
169
+ const opts = { serverURL: 'https://api.example.com' };
170
+ const result = hook.sdkInit(opts);
171
+ // When cert file is missing, no httpClient should be configured
172
+ expect(result).toBe(opts);
173
+ expect(result.httpClient).toBeUndefined();
174
+ });
175
+ test('test sdkInit returns original opts when client key file missing', () => {
176
+ GalileoConfig.reset();
177
+ GalileoConfig.get({
178
+ apiKey: 'test-key',
179
+ apiUrl: 'https://api.example.com',
180
+ caCertPath,
181
+ clientCertPath,
182
+ clientKeyPath: '/nonexistent/key.pem',
183
+ });
184
+ const hook = new CertManagementHook();
185
+ const opts = { serverURL: 'https://api.example.com' };
186
+ const result = hook.sdkInit(opts);
187
+ expect(result).toBe(opts);
188
+ expect(result.httpClient).toBeUndefined();
189
+ });
190
+ test('test sdkInit returns original opts when only client cert configured', () => {
191
+ GalileoConfig.reset();
192
+ GalileoConfig.get({
193
+ apiKey: 'test-key',
194
+ apiUrl: 'https://api.example.com',
195
+ caCertPath,
196
+ clientCertPath,
197
+ });
198
+ const hook = new CertManagementHook();
199
+ const opts = { serverURL: 'https://api.example.com' };
200
+ const result = hook.sdkInit(opts);
201
+ // When only client cert is provided (missing key), should return original opts
202
+ expect(result).toBe(opts);
203
+ expect(result.httpClient).toBeUndefined();
204
+ });
205
+ test('test sdkInit returns original opts when only client key configured', () => {
206
+ GalileoConfig.reset();
207
+ GalileoConfig.get({
208
+ apiKey: 'test-key',
209
+ apiUrl: 'https://api.example.com',
210
+ caCertPath,
211
+ clientKeyPath,
212
+ });
213
+ const hook = new CertManagementHook();
214
+ const opts = { serverURL: 'https://api.example.com' };
215
+ const result = hook.sdkInit(opts);
216
+ // When only client key is provided (missing cert), should return original opts
217
+ expect(result).toBe(opts);
218
+ expect(result.httpClient).toBeUndefined();
219
+ });
220
+ test('test sdkInit returns original opts when client cert file missing', () => {
221
+ GalileoConfig.reset();
222
+ GalileoConfig.get({
223
+ apiKey: 'test-key',
224
+ apiUrl: 'https://api.example.com',
225
+ caCertPath,
226
+ clientCertPath: '/nonexistent/cert.pem',
227
+ clientKeyPath,
228
+ });
229
+ const hook = new CertManagementHook();
230
+ const opts = { serverURL: 'https://api.example.com' };
231
+ const result = hook.sdkInit(opts);
232
+ expect(result).toBe(opts);
233
+ expect(result.httpClient).toBeUndefined();
234
+ });
235
+ });
236
+ describe('environment variable priority', () => {
237
+ test('test GALILEO_CA_CERT_PATH from env is used', () => {
238
+ GalileoConfig.reset();
239
+ process.env['GALILEO_API_KEY'] = 'env-key';
240
+ process.env['GALILEO_CA_CERT_PATH'] = caCertPath;
241
+ const config = GalileoConfig.get({});
242
+ const cert = config.getCertConfig();
243
+ expect(cert).not.toBeNull();
244
+ if (!cert)
245
+ throw new Error("unreachable");
246
+ expect(cert.caCertPath).toBe(caCertPath);
247
+ });
248
+ test('test only GALILEO_CA_CERT_PATH is supported for CA cert from env', () => {
249
+ GalileoConfig.reset();
250
+ process.env['GALILEO_API_KEY'] = 'env-key';
251
+ process.env['SSL_CERT_FILE'] = caCertPath;
252
+ const config = GalileoConfig.get({});
253
+ const cert = config.getCertConfig();
254
+ expect(cert).toBeNull();
255
+ });
256
+ test('test only GALILEO_CA_CERT_PATH is supported not NODE_EXTRA_CA_CERTS', () => {
257
+ GalileoConfig.reset();
258
+ process.env['GALILEO_API_KEY'] = 'env-key';
259
+ process.env['NODE_EXTRA_CA_CERTS'] = caCertPath;
260
+ const config = GalileoConfig.get({});
261
+ const cert = config.getCertConfig();
262
+ expect(cert).toBeNull();
263
+ });
264
+ });
265
+ describe('client certificate environment variables', () => {
266
+ test('test GALILEO_CLIENT_CERT_PATH and GALILEO_CLIENT_KEY_PATH from env', () => {
267
+ GalileoConfig.reset();
268
+ process.env['GALILEO_API_KEY'] = 'env-key';
269
+ process.env['GALILEO_CA_CERT_PATH'] = caCertPath;
270
+ process.env['GALILEO_CLIENT_CERT_PATH'] = clientCertPath;
271
+ process.env['GALILEO_CLIENT_KEY_PATH'] = clientKeyPath;
272
+ const config = GalileoConfig.get({});
273
+ const cert = config.getCertConfig();
274
+ expect(cert).not.toBeNull();
275
+ if (!cert)
276
+ throw new Error("unreachable");
277
+ expect(cert.clientCertPath).toBe(clientCertPath);
278
+ expect(cert.clientKeyPath).toBe(clientKeyPath);
279
+ });
280
+ });
281
+ describe('rejectUnauthorized configuration', () => {
282
+ test('test GALILEO_REJECT_UNAUTHORIZED takes priority over NODE_TLS_REJECT_UNAUTHORIZED', () => {
283
+ GalileoConfig.reset();
284
+ process.env['GALILEO_API_KEY'] = 'env-key';
285
+ process.env['GALILEO_CA_CERT_PATH'] = caCertPath;
286
+ process.env['GALILEO_REJECT_UNAUTHORIZED'] = 'false';
287
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '1';
288
+ const config = GalileoConfig.get({});
289
+ const cert = config.getCertConfig();
290
+ expect(cert).not.toBeNull();
291
+ if (!cert)
292
+ throw new Error("unreachable");
293
+ expect(cert.rejectUnauthorized).toBe(false);
294
+ });
295
+ test('test NODE_TLS_REJECT_UNAUTHORIZED used when GALILEO_REJECT_UNAUTHORIZED absent', () => {
296
+ GalileoConfig.reset();
297
+ process.env['GALILEO_API_KEY'] = 'env-key';
298
+ process.env['GALILEO_CA_CERT_PATH'] = caCertPath;
299
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
300
+ const config = GalileoConfig.get({});
301
+ const cert = config.getCertConfig();
302
+ expect(cert).not.toBeNull();
303
+ if (!cert)
304
+ throw new Error("unreachable");
305
+ expect(cert.rejectUnauthorized).toBe(false);
306
+ });
307
+ test('test rejectUnauthorized defaults to true when no env vars set', () => {
308
+ GalileoConfig.reset();
309
+ const config = GalileoConfig.get({
310
+ apiKey: 'test-key',
311
+ apiUrl: 'https://api.example.com',
312
+ caCertPath,
313
+ });
314
+ const cert = config.getCertConfig();
315
+ expect(cert).not.toBeNull();
316
+ if (!cert)
317
+ throw new Error("unreachable");
318
+ // rejectUnauthorized is undefined in config, defaults to true in hook
319
+ expect(cert.rejectUnauthorized).toBeUndefined();
320
+ });
321
+ test('test sdkInit configures agent with custom CA cert even when rejectUnauthorized is true', () => {
322
+ GalileoConfig.reset();
323
+ GalileoConfig.get({
324
+ apiKey: 'test-key',
325
+ apiUrl: 'https://api.example.com',
326
+ caCertPath,
327
+ rejectUnauthorized: true,
328
+ });
329
+ const hook = new CertManagementHook();
330
+ const opts = { serverURL: 'https://api.example.com' };
331
+ const result = hook.sdkInit(opts);
332
+ // Custom CA cert should be configured regardless of rejectUnauthorized value
333
+ // rejectUnauthorized=true means strict validation with custom CA; this is a valid configuration
334
+ expect(result.httpClient).toBeDefined();
335
+ expect(result.httpClient).not.toBe(opts.httpClient);
336
+ });
337
+ test('test rejectUnauthorized false is passed to connectOptions', () => {
338
+ GalileoConfig.reset();
339
+ GalileoConfig.get({
340
+ apiKey: 'test-key',
341
+ apiUrl: 'https://api.example.com',
342
+ caCertPath,
343
+ rejectUnauthorized: false,
344
+ });
345
+ const hook = new CertManagementHook();
346
+ const opts = { serverURL: 'https://api.example.com' };
347
+ const result = hook.sdkInit(opts);
348
+ expect(result.httpClient).toBeDefined();
349
+ expect(result.httpClient).not.toBe(opts.httpClient);
350
+ });
351
+ });
352
+ describe('CertAgent availability', () => {
353
+ test('test sdkInit returns original opts when CertAgent is unavailable', () => {
354
+ // Mock isNodeLike to return true but simulate missing undici by not being in Node-like environment for Agent
355
+ // This tests the guard at line 53: if (!isNodeLike() || !CertAgent)
356
+ GalileoConfig.reset();
357
+ GalileoConfig.get({
358
+ apiKey: 'test-key',
359
+ apiUrl: 'https://api.example.com',
360
+ caCertPath,
361
+ });
362
+ // Simulate CertAgent being undefined by mocking the module reload scenario
363
+ const hook = new CertManagementHook();
364
+ const opts = { serverURL: 'https://api.example.com' };
365
+ // This test verifies that even with valid config, if CertAgent is not available,
366
+ // the hook gracefully returns original opts
367
+ // Note: In real scenario, CertAgent would be undefined if undici import fails
368
+ const result = hook.sdkInit(opts);
369
+ // If undici is available (which it should be in test environment), httpClient should be created
370
+ // If undici is not available, result should be opts
371
+ expect(result).toBeDefined();
372
+ expect(result.serverURL).toBe(opts.serverURL);
373
+ });
374
+ });
375
+ describe('integration - certificate mechanism', () => {
376
+ test('test TLS hook is added via beforeRequest hook', async () => {
377
+ GalileoConfig.reset();
378
+ GalileoConfig.get({
379
+ apiKey: 'test-key',
380
+ apiUrl: 'https://api.example.com',
381
+ caCertPath,
382
+ });
383
+ const hook = new CertManagementHook();
384
+ const mockFetcher = vi.fn().mockResolvedValue(new Response('OK'));
385
+ const httpClient = new HTTPClient({ fetcher: mockFetcher });
386
+ const opts = { httpClient };
387
+ const result = hook.sdkInit(opts);
388
+ expect(result.httpClient).toBe(httpClient);
389
+ // Make a GET request (no body) through the augmented client
390
+ const request = new Request('https://api.example.com/test', {
391
+ method: 'GET',
392
+ headers: { 'Content-Type': 'application/json' },
393
+ });
394
+ const response = await result.httpClient?.request(request);
395
+ // Verify the custom fetcher was called
396
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
397
+ const callArgs = mockFetcher.mock.calls[0];
398
+ expect(callArgs).toBeDefined();
399
+ if (!callArgs)
400
+ throw new Error('unreachable');
401
+ const [calledReq] = callArgs;
402
+ // Verify the request properties are preserved
403
+ expect(calledReq).toBeInstanceOf(Request);
404
+ expect(calledReq.url).toBe('https://api.example.com/test');
405
+ expect(calledReq.method).toBe('GET');
406
+ expect(calledReq.headers.get('Content-Type')).toBe('application/json');
407
+ // Verify response was returned
408
+ expect(response?.status).toBe(200);
409
+ });
410
+ test('test httpClient uses undici dispatcher with certificates', async () => {
411
+ GalileoConfig.reset();
412
+ GalileoConfig.get({
413
+ apiKey: 'test-key',
414
+ apiUrl: 'https://api.example.com',
415
+ caCertPath,
416
+ });
417
+ const hook = new CertManagementHook();
418
+ // Use a custom fetcher that captures what it receives
419
+ let receivedRequest = null;
420
+ const testFetcher = vi.fn(async (input, _init) => {
421
+ if (input instanceof Request) {
422
+ receivedRequest = input;
423
+ }
424
+ return new Response(JSON.stringify({ success: true }), {
425
+ status: 200,
426
+ headers: { 'Content-Type': 'application/json' },
427
+ });
428
+ });
429
+ const httpClient = new HTTPClient({ fetcher: testFetcher });
430
+ const opts = { httpClient };
431
+ const result = hook.sdkInit(opts);
432
+ expect(result.httpClient).toBeDefined();
433
+ // Make a request using the httpClient
434
+ const request = new Request('https://api.example.com/test', {
435
+ method: 'GET',
436
+ headers: { 'Content-Type': 'application/json' },
437
+ });
438
+ await result.httpClient?.request(request);
439
+ // Verify the custom fetcher was called
440
+ expect(testFetcher).toHaveBeenCalledTimes(1);
441
+ expect(receivedRequest).toBeDefined();
442
+ // The hook should have transformed the request
443
+ if (!receivedRequest)
444
+ throw new Error('unreachable');
445
+ expect(receivedRequest).toBeInstanceOf(Request);
446
+ expect(receivedRequest.url).toBe('https://api.example.com/test');
447
+ });
448
+ test('test httpClient with certificates can make successful requests', async () => {
449
+ GalileoConfig.reset();
450
+ GalileoConfig.get({
451
+ apiKey: 'test-key',
452
+ apiUrl: 'https://api.example.com',
453
+ caCertPath,
454
+ rejectUnauthorized: false, // For testing purposes
455
+ });
456
+ const hook = new CertManagementHook();
457
+ const opts = { serverURL: 'https://api.example.com' };
458
+ const result = hook.sdkInit(opts);
459
+ expect(result.httpClient).toBeDefined();
460
+ // Mock successful response
461
+ const mockResponse = new Response(JSON.stringify({ data: 'test' }), {
462
+ status: 200,
463
+ headers: { 'Content-Type': 'application/json' },
464
+ });
465
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse);
466
+ // Make a GET request (no body)
467
+ const request = new Request('https://api.example.com/endpoint', {
468
+ method: 'GET',
469
+ headers: { 'Content-Type': 'application/json' },
470
+ });
471
+ const response = await result.httpClient?.request(request);
472
+ // Verify request succeeded
473
+ expect(response).toBeDefined();
474
+ expect(response?.status).toBe(200);
475
+ expect(fetchSpy).toHaveBeenCalled();
476
+ fetchSpy.mockRestore();
477
+ });
478
+ test('test httpClient created with mTLS configuration without CA cert', async () => {
479
+ GalileoConfig.reset();
480
+ GalileoConfig.get({
481
+ apiKey: 'test-key',
482
+ apiUrl: 'https://api.example.com',
483
+ clientCertPath,
484
+ clientKeyPath,
485
+ });
486
+ const hook = new CertManagementHook();
487
+ const mockFetcher = vi.fn().mockResolvedValue(new Response(JSON.stringify({ success: true }), { status: 200 }));
488
+ const httpClient = new HTTPClient({ fetcher: mockFetcher });
489
+ const opts = { httpClient };
490
+ const result = hook.sdkInit(opts);
491
+ expect(result.httpClient).toBeDefined();
492
+ // With augmentation approach, same instance is returned
493
+ expect(result.httpClient).toBe(opts.httpClient);
494
+ // Verify the httpClient can make requests with the configured certificates
495
+ const request = new Request('https://api.example.com/test', {
496
+ method: 'GET',
497
+ });
498
+ await result.httpClient?.request(request);
499
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
500
+ const callArgs = mockFetcher.mock.calls[0];
501
+ expect(callArgs).toBeDefined();
502
+ if (!callArgs)
503
+ throw new Error('unreachable');
504
+ const [calledReq] = callArgs;
505
+ expect(calledReq).toBeInstanceOf(Request);
506
+ expect(calledReq.url).toBe('https://api.example.com/test');
507
+ });
508
+ test('test sdkInit returns original opts when no meaningful TLS customization', () => {
509
+ GalileoConfig.reset();
510
+ GalileoConfig.get({
511
+ apiKey: 'test-key',
512
+ apiUrl: 'https://api.example.com',
513
+ rejectUnauthorized: true,
514
+ });
515
+ const hook = new CertManagementHook();
516
+ const opts = { serverURL: 'https://api.example.com' };
517
+ const result = hook.sdkInit(opts);
518
+ // When only rejectUnauthorized=true is set (no CA/client certs), hasCertCustomization is false
519
+ // because hasCertCustomization requires CA, cert, key, or rejectUnauthorized === false (line 118)
520
+ expect(result).toBe(opts);
521
+ expect(result.httpClient).toBeUndefined();
522
+ });
523
+ test('test sdkInit configures mTLS with client certs even when rejectUnauthorized is true', () => {
524
+ GalileoConfig.reset();
525
+ GalileoConfig.get({
526
+ apiKey: 'test-key',
527
+ apiUrl: 'https://api.example.com',
528
+ clientCertPath,
529
+ clientKeyPath,
530
+ rejectUnauthorized: true,
531
+ });
532
+ const hook = new CertManagementHook();
533
+ const opts = { serverURL: 'https://api.example.com' };
534
+ const result = hook.sdkInit(opts);
535
+ // mTLS client certs should be configured regardless of rejectUnauthorized value
536
+ // rejectUnauthorized and custom certs are orthogonal concerns
537
+ expect(result.httpClient).toBeDefined();
538
+ });
539
+ });
540
+ describe('user hook preservation', () => {
541
+ test('test user-registered hooks are preserved when TLS is configured', async () => {
542
+ GalileoConfig.reset();
543
+ GalileoConfig.get({
544
+ apiKey: 'test-key',
545
+ apiUrl: 'https://api.example.com',
546
+ caCertPath,
547
+ });
548
+ let userHookCalled = false;
549
+ const mockFetcher = vi.fn().mockResolvedValue(new Response('OK'));
550
+ const userClient = new HTTPClient({ fetcher: mockFetcher });
551
+ userClient.addHook('beforeRequest', (req) => {
552
+ userHookCalled = true;
553
+ // User hook can modify headers
554
+ const newReq = new Request(req.url, {
555
+ method: req.method,
556
+ headers: req.headers,
557
+ body: req.body,
558
+ });
559
+ newReq.headers.set('X-Custom-Header', 'user-value');
560
+ return newReq;
561
+ });
562
+ const hook = new CertManagementHook();
563
+ const opts = { httpClient: userClient };
564
+ const result = hook.sdkInit(opts);
565
+ const request = new Request('https://api.example.com/test', {
566
+ method: 'GET',
567
+ });
568
+ await result.httpClient?.request(request);
569
+ // Verify user hook was called
570
+ expect(userHookCalled).toBe(true);
571
+ // Verify the custom fetcher was called
572
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
573
+ const callArgs = mockFetcher.mock.calls[0];
574
+ expect(callArgs).toBeDefined();
575
+ if (!callArgs)
576
+ throw new Error('unreachable');
577
+ const [calledReq] = callArgs;
578
+ // Verify user's header was preserved in the final request
579
+ expect(calledReq).toBeInstanceOf(Request);
580
+ expect(calledReq.headers.get('X-Custom-Header')).toBe('user-value');
581
+ });
582
+ test('test TLS hook runs after user hooks', async () => {
583
+ GalileoConfig.reset();
584
+ GalileoConfig.get({
585
+ apiKey: 'test-key',
586
+ apiUrl: 'https://api.example.com',
587
+ caCertPath,
588
+ });
589
+ const callOrder = [];
590
+ const userClient = new HTTPClient();
591
+ userClient.addHook('beforeRequest', (req) => {
592
+ callOrder.push('user-hook');
593
+ return req;
594
+ });
595
+ const hook = new CertManagementHook();
596
+ const opts = { httpClient: userClient };
597
+ const result = hook.sdkInit(opts);
598
+ // Mock fetch to capture when it's called
599
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
600
+ callOrder.push('fetch');
601
+ return new Response('OK');
602
+ });
603
+ const request = new Request('https://api.example.com/test', {
604
+ method: 'GET',
605
+ });
606
+ await result.httpClient?.request(request);
607
+ // User hook should run before fetch (and before TLS hook which is closest to fetch)
608
+ expect(callOrder).toEqual(['user-hook', 'fetch']);
609
+ fetchSpy.mockRestore();
610
+ });
611
+ test('test multiple user hooks are all executed with TLS', async () => {
612
+ GalileoConfig.reset();
613
+ GalileoConfig.get({
614
+ apiKey: 'test-key',
615
+ apiUrl: 'https://api.example.com',
616
+ caCertPath,
617
+ });
618
+ const mockFetcher = vi.fn().mockResolvedValue(new Response('OK'));
619
+ const userClient = new HTTPClient({ fetcher: mockFetcher });
620
+ const calls = [];
621
+ userClient.addHook('beforeRequest', (req) => {
622
+ calls.push('hook1');
623
+ return req;
624
+ });
625
+ userClient.addHook('beforeRequest', (req) => {
626
+ calls.push('hook2');
627
+ return req;
628
+ });
629
+ const hook = new CertManagementHook();
630
+ const opts = { httpClient: userClient };
631
+ const result = hook.sdkInit(opts);
632
+ const request = new Request('https://api.example.com/test', {
633
+ method: 'GET',
634
+ });
635
+ await result.httpClient?.request(request);
636
+ // Both user hooks should be called
637
+ expect(calls).toContain('hook1');
638
+ expect(calls).toContain('hook2');
639
+ // And the fetcher should be called with a request
640
+ expect(mockFetcher).toHaveBeenCalled();
641
+ const callArgs = mockFetcher.mock.calls[0];
642
+ expect(callArgs).toBeDefined();
643
+ if (!callArgs)
644
+ throw new Error('unreachable');
645
+ const [calledReq] = callArgs;
646
+ expect(calledReq).toBeInstanceOf(Request);
647
+ });
648
+ });
649
+ describe('request body handling', () => {
650
+ test('test TLS hook preserves request headers and url', async () => {
651
+ GalileoConfig.reset();
652
+ GalileoConfig.get({
653
+ apiKey: 'test-key',
654
+ apiUrl: 'https://api.example.com',
655
+ caCertPath,
656
+ });
657
+ const hook = new CertManagementHook();
658
+ const opts = {};
659
+ const result = hook.sdkInit(opts);
660
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('OK'));
661
+ const request = new Request('https://api.example.com/test', {
662
+ method: 'GET',
663
+ headers: { 'X-Custom': 'value', 'Content-Type': 'application/json' },
664
+ });
665
+ await result.httpClient?.request(request);
666
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
667
+ const callArgs = fetchSpy.mock.calls[0];
668
+ expect(callArgs).toBeDefined();
669
+ if (!callArgs)
670
+ throw new Error('unreachable');
671
+ const [calledReq] = callArgs;
672
+ expect(calledReq.url).toBe('https://api.example.com/test');
673
+ expect(calledReq.headers.get('X-Custom')).toBe('value');
674
+ expect(calledReq.headers.get('Content-Type')).toBe('application/json');
675
+ fetchSpy.mockRestore();
676
+ });
677
+ });
678
+ describe('runtime version detection and warnings', () => {
679
+ test('test version detection correctly identifies supported Node.js versions', () => {
680
+ GalileoConfig.reset();
681
+ GalileoConfig.get({
682
+ apiKey: 'test-key',
683
+ apiUrl: 'https://api.example.com',
684
+ caCertPath,
685
+ });
686
+ const hook = new CertManagementHook();
687
+ // Access the private method via type casting for testing
688
+ const hookAny = hook;
689
+ // Test various version strings
690
+ expect(hookAny.isNodeVersionSupported('20.18.0')).toBe(false); // Minor < 18
691
+ expect(hookAny.isNodeVersionSupported('20.18.1')).toBe(true); // Exact minimum
692
+ expect(hookAny.isNodeVersionSupported('20.19.0')).toBe(true); // Minor > 18
693
+ expect(hookAny.isNodeVersionSupported('21.0.0')).toBe(true); // Major > 20
694
+ expect(hookAny.isNodeVersionSupported('22.5.0')).toBe(true); // Much newer
695
+ expect(hookAny.isNodeVersionSupported('19.10.0')).toBe(false); // Too old
696
+ });
697
+ test('test version string parsing handles various formats', () => {
698
+ GalileoConfig.reset();
699
+ GalileoConfig.get({
700
+ apiKey: 'test-key',
701
+ apiUrl: 'https://api.example.com',
702
+ caCertPath,
703
+ });
704
+ const hook = new CertManagementHook();
705
+ const hookAny = hook;
706
+ // Test edge cases
707
+ expect(hookAny.isNodeVersionSupported('20')).toBe(false); // No minor version (defaults to 0)
708
+ expect(hookAny.isNodeVersionSupported('20.18')).toBe(false); // No patch version (defaults to 0, which is < 1)
709
+ expect(hookAny.isNodeVersionSupported('20.19')).toBe(true); // Minor > 18
710
+ // Note: parseInt('invalid') returns NaN, but parts[0] would be NaN which !== undefined
711
+ // so the check major === undefined won't catch it. It would return false.
712
+ // For truly invalid versions, they'd fail the >= check anyway
713
+ expect(hookAny.isNodeVersionSupported('')).toBe(true); // Empty string → optimistic
714
+ });
715
+ test('test Node.js version extraction from process.versions', () => {
716
+ GalileoConfig.reset();
717
+ GalileoConfig.get({
718
+ apiKey: 'test-key',
719
+ apiUrl: 'https://api.example.com',
720
+ caCertPath,
721
+ });
722
+ const hook = new CertManagementHook();
723
+ const hookAny = hook;
724
+ // Get the actual Node version
725
+ const version = hookAny.getNodeVersion();
726
+ // Verify we got a version string (or null in non-Node environments)
727
+ if (version !== null) {
728
+ expect(typeof version).toBe('string');
729
+ expect(version.length).toBeGreaterThan(0);
730
+ }
731
+ });
732
+ });
733
+ describe('dispatcher integration', () => {
734
+ test('test dispatcher is passed correctly through the request chain', async () => {
735
+ GalileoConfig.reset();
736
+ GalileoConfig.get({
737
+ apiKey: 'test-key',
738
+ apiUrl: 'https://api.example.com',
739
+ caCertPath,
740
+ });
741
+ const hook = new CertManagementHook();
742
+ const mockFetcher = vi.fn().mockResolvedValue(new Response('OK'));
743
+ const httpClient = new HTTPClient({ fetcher: mockFetcher });
744
+ const opts = { httpClient };
745
+ const result = hook.sdkInit(opts);
746
+ const request = new Request('https://api.example.com/test', {
747
+ method: 'GET',
748
+ });
749
+ await result.httpClient?.request(request);
750
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
751
+ const callArgs = mockFetcher.mock.calls[0];
752
+ expect(callArgs).toBeDefined();
753
+ if (!callArgs)
754
+ throw new Error('unreachable');
755
+ const [calledReq] = callArgs;
756
+ // Dispatcher should be attached to the request object
757
+ // (In a real Node.js environment with undici, this would be used by fetch)
758
+ expect(calledReq).toBeInstanceOf(Request);
759
+ // Verify it's a proper Request with all properties
760
+ expect(calledReq.url).toBeDefined();
761
+ expect(calledReq.method).toBeDefined();
762
+ expect(calledReq.headers).toBeDefined();
763
+ });
764
+ test('test TLS hook creates new Request instances', async () => {
765
+ GalileoConfig.reset();
766
+ GalileoConfig.get({
767
+ apiKey: 'test-key',
768
+ apiUrl: 'https://api.example.com',
769
+ caCertPath,
770
+ });
771
+ const hook = new CertManagementHook();
772
+ const mockFetcher = vi.fn().mockResolvedValue(new Response('OK'));
773
+ const httpClient = new HTTPClient({ fetcher: mockFetcher });
774
+ const opts = { httpClient };
775
+ const result = hook.sdkInit(opts);
776
+ const originalRequest = new Request('https://api.example.com/test', {
777
+ method: 'GET',
778
+ });
779
+ await result.httpClient?.request(originalRequest);
780
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
781
+ const callArgs = mockFetcher.mock.calls[0];
782
+ expect(callArgs).toBeDefined();
783
+ if (!callArgs)
784
+ throw new Error('unreachable');
785
+ const [calledReq] = callArgs;
786
+ // The request passed to the fetcher should be a new instance
787
+ // (Request objects are immutable, so the hook creates a new one)
788
+ expect(calledReq).not.toBe(originalRequest);
789
+ });
790
+ });
791
+ });
792
+ //# sourceMappingURL=cert-management.test.js.map