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