@continuedev/fetch 1.0.15 → 1.1.0

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.
@@ -0,0 +1,356 @@
1
+ import * as fs from "node:fs";
2
+ import * as http from "node:http";
3
+ import * as https from "node:https";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ import { afterEach, describe, expect, test } from "vitest";
8
+ import { fetchwithRequestOptions } from "./fetch.js";
9
+ // Test server ports
10
+ const HTTP_PORT = 3001;
11
+ const HTTPS_PORT = 3002;
12
+ // Track servers and temp dirs for cleanup
13
+ const serversToCleanup = [];
14
+ const tempDirsToCleanup = [];
15
+ afterEach(() => {
16
+ // Clean up all servers
17
+ serversToCleanup.forEach((server) => server.close());
18
+ serversToCleanup.length = 0;
19
+ // Clean up temp directories
20
+ tempDirsToCleanup.forEach((tempDir) => {
21
+ if (fs.existsSync(tempDir)) {
22
+ fs.rmSync(tempDir, { recursive: true, force: true });
23
+ }
24
+ });
25
+ tempDirsToCleanup.length = 0;
26
+ });
27
+ /**
28
+ * Create a temporary directory for test files
29
+ */
30
+ function createTempDir() {
31
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fetch-e2e-test-"));
32
+ tempDirsToCleanup.push(tempDir);
33
+ return tempDir;
34
+ }
35
+ /**
36
+ * Generate a self-signed certificate for testing
37
+ */
38
+ async function generateCertificate(tempDir, name, options) {
39
+ const certPath = path.join(tempDir, `${name}.crt`);
40
+ const keyPath = path.join(tempDir, `${name}.key`);
41
+ const caCertPath = path.join(tempDir, `${name}-ca.crt`);
42
+ try {
43
+ // Generate a private key
44
+ execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: "pipe" });
45
+ // Create config file for certificate
46
+ const configPath = path.join(tempDir, `${name}.conf`);
47
+ const altNames = options.san
48
+ .map((san, i) => {
49
+ return san.match(/^\d+\.\d+\.\d+\.\d+$/)
50
+ ? `IP.${i + 1} = ${san}`
51
+ : `DNS.${i + 1} = ${san}`;
52
+ })
53
+ .join("\n");
54
+ const configContent = `
55
+ [req]
56
+ distinguished_name = req_distinguished_name
57
+ req_extensions = v3_req
58
+ prompt = no
59
+
60
+ [req_distinguished_name]
61
+ C = US
62
+ ST = Test
63
+ L = Test
64
+ O = ${options.org}
65
+ CN = ${options.cn}
66
+
67
+ [v3_req]
68
+ keyUsage = keyEncipherment, dataEncipherment
69
+ extendedKeyUsage = serverAuth
70
+ subjectAltName = @alt_names
71
+
72
+ [alt_names]
73
+ ${altNames}
74
+ `;
75
+ fs.writeFileSync(configPath, configContent);
76
+ // Generate a self-signed certificate
77
+ execSync(`openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -config "${configPath}" -extensions v3_req`, { stdio: "pipe" });
78
+ // Copy the certificate as CA (for testing custom CA scenarios)
79
+ fs.copyFileSync(certPath, caCertPath);
80
+ return { certPath, keyPath, caCertPath };
81
+ }
82
+ catch (error) {
83
+ // Fallback: create minimal test certificates if OpenSSL not available
84
+ const key = `-----BEGIN PRIVATE KEY-----
85
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7VJTUt9Us8cKB
86
+ wEiOfniel+2jNcJjYUiUoq5YbVKk+xqt4bOMh5DNFJ3LnU1OaUHyG5sHlgNyKA==
87
+ -----END PRIVATE KEY-----`;
88
+ const cert = `-----BEGIN CERTIFICATE-----
89
+ MIICljCCAX4CCQCKnW9qX7TlxzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV
90
+ UzAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMA0xCzAJBgNVBAYTAlVT
91
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1L7VLPHCQD1OvF4p
92
+ 4td9ozXCY2FIlKKuWG1SpPsareGzjIeQzRSdy51NTmlB8hubB5YDcigN8=
93
+ -----END CERTIFICATE-----`;
94
+ fs.writeFileSync(keyPath, key);
95
+ fs.writeFileSync(certPath, cert);
96
+ fs.writeFileSync(caCertPath, cert);
97
+ return { certPath, keyPath, caCertPath };
98
+ }
99
+ }
100
+ /**
101
+ * Start a simple HTTP server for testing
102
+ */
103
+ async function startHttpServer() {
104
+ const server = http.createServer((req, res) => {
105
+ const url = new URL(req.url, `http://localhost:${HTTP_PORT}`);
106
+ if (url.pathname === "/json") {
107
+ res.writeHead(200, { "Content-Type": "application/json" });
108
+ res.end(JSON.stringify({
109
+ message: "Hello from HTTP server",
110
+ method: req.method,
111
+ }));
112
+ }
113
+ else if (url.pathname === "/headers") {
114
+ res.writeHead(200, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ headers: req.headers }));
116
+ }
117
+ else if (url.pathname === "/body") {
118
+ let body = "";
119
+ req.on("data", (chunk) => {
120
+ body += chunk.toString();
121
+ });
122
+ req.on("end", () => {
123
+ res.writeHead(200, { "Content-Type": "application/json" });
124
+ res.end(JSON.stringify({ body, headers: req.headers }));
125
+ });
126
+ }
127
+ else if (url.pathname === "/status/404") {
128
+ res.writeHead(404, { "Content-Type": "text/plain" });
129
+ res.end("Not Found");
130
+ }
131
+ else if (url.pathname === "/slow") {
132
+ setTimeout(() => {
133
+ res.writeHead(200, { "Content-Type": "text/plain" });
134
+ res.end("Slow response");
135
+ }, 100);
136
+ }
137
+ else {
138
+ res.writeHead(404);
139
+ res.end("Not Found");
140
+ }
141
+ });
142
+ await new Promise((resolve) => {
143
+ server.listen(HTTP_PORT, () => resolve());
144
+ });
145
+ serversToCleanup.push(server);
146
+ return server;
147
+ }
148
+ /**
149
+ * Start an HTTPS server with given certificates
150
+ */
151
+ async function startHttpsServer(certPath, keyPath) {
152
+ const server = https.createServer({
153
+ cert: fs.readFileSync(certPath),
154
+ key: fs.readFileSync(keyPath),
155
+ }, (req, res) => {
156
+ const url = new URL(req.url, `https://localhost:${HTTPS_PORT}`);
157
+ if (url.pathname === "/secure") {
158
+ res.writeHead(200, { "Content-Type": "application/json" });
159
+ res.end(JSON.stringify({ message: "Hello from HTTPS server", secure: true }));
160
+ }
161
+ else if (url.pathname === "/headers") {
162
+ res.writeHead(200, { "Content-Type": "application/json" });
163
+ res.end(JSON.stringify({ headers: req.headers }));
164
+ }
165
+ else {
166
+ res.writeHead(404);
167
+ res.end("Not Found");
168
+ }
169
+ });
170
+ await new Promise((resolve) => {
171
+ server.listen(HTTPS_PORT, () => resolve());
172
+ });
173
+ serversToCleanup.push(server);
174
+ return server;
175
+ }
176
+ describe("fetchwithRequestOptions E2E tests", () => {
177
+ describe("HTTP requests", () => {
178
+ test("should make basic HTTP GET request", async () => {
179
+ await startHttpServer();
180
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/json`);
181
+ expect(response.status).toBe(200);
182
+ const data = (await response.json());
183
+ expect(data).toEqual({
184
+ message: "Hello from HTTP server",
185
+ method: "GET",
186
+ });
187
+ });
188
+ test("should make HTTP POST request with body", async () => {
189
+ await startHttpServer();
190
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/json`, {
191
+ method: "POST",
192
+ body: JSON.stringify({ test: "data" }),
193
+ headers: { "Content-Type": "application/json" },
194
+ });
195
+ expect(response.status).toBe(200);
196
+ const data = (await response.json());
197
+ expect(data.method).toBe("POST");
198
+ });
199
+ test("should handle custom headers", async () => {
200
+ await startHttpServer();
201
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/headers`, {
202
+ headers: { "X-Custom-Header": "test-value" },
203
+ });
204
+ const data = (await response.json());
205
+ expect(data.headers["x-custom-header"]).toBe("test-value");
206
+ });
207
+ test("should handle error responses", async () => {
208
+ await startHttpServer();
209
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/status/404`);
210
+ expect(response.status).toBe(404);
211
+ expect(response.ok).toBe(false);
212
+ });
213
+ });
214
+ describe("Request options integration", () => {
215
+ test("should merge extra body properties", async () => {
216
+ await startHttpServer();
217
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/body`, {
218
+ method: "POST",
219
+ body: JSON.stringify({ original: "data" }),
220
+ headers: { "Content-Type": "application/json" },
221
+ }, {
222
+ extraBodyProperties: { extra: "property" },
223
+ });
224
+ expect(response.status).toBe(200);
225
+ const data = (await response.json());
226
+ const body = JSON.parse(data.body);
227
+ expect(body).toEqual({ original: "data", extra: "property" });
228
+ });
229
+ test("should handle RequestOptions headers", async () => {
230
+ await startHttpServer();
231
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/headers`, {}, {
232
+ headers: { "X-Request-Options-Header": "from-request-options" },
233
+ });
234
+ const data = (await response.json());
235
+ expect(data.headers["x-request-options-header"]).toBe("from-request-options");
236
+ });
237
+ test("should merge headers from init and requestOptions", async () => {
238
+ await startHttpServer();
239
+ const response = await fetchwithRequestOptions(`http://localhost:${HTTP_PORT}/headers`, {
240
+ headers: { "X-Init-Header": "from-init" },
241
+ }, {
242
+ headers: { "X-Request-Options-Header": "from-request-options" },
243
+ });
244
+ const data = (await response.json());
245
+ expect(data.headers["x-init-header"]).toBe("from-init");
246
+ expect(data.headers["x-request-options-header"]).toBe("from-request-options");
247
+ });
248
+ });
249
+ describe("Certificate handling - Enterprise scenarios", () => {
250
+ test("should REJECT self-signed certificates when SSL verification is enabled", async () => {
251
+ const tempDir = createTempDir();
252
+ const serverCerts = await generateCertificate(tempDir, "server", {
253
+ cn: "localhost",
254
+ san: ["localhost", "127.0.0.1"],
255
+ org: "Test",
256
+ });
257
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
258
+ // This simulates the customer's "unable to get local issuer certificate" error
259
+ await expect(fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, { verifySsl: true })).rejects.toThrow(); // Should fail with certificate error
260
+ });
261
+ test("should ACCEPT self-signed certificates when SSL verification is disabled", async () => {
262
+ const tempDir = createTempDir();
263
+ const serverCerts = await generateCertificate(tempDir, "server", {
264
+ cn: "localhost",
265
+ san: ["localhost", "127.0.0.1"],
266
+ org: "Test",
267
+ });
268
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
269
+ const response = await fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, { verifySsl: false });
270
+ expect(response.status).toBe(200);
271
+ const data = (await response.json());
272
+ expect(data.secure).toBe(true);
273
+ });
274
+ test("should ACCEPT self-signed certificates with custom CA bundle", async () => {
275
+ const tempDir = createTempDir();
276
+ const serverCerts = await generateCertificate(tempDir, "server", {
277
+ cn: "localhost",
278
+ san: ["localhost", "127.0.0.1"],
279
+ org: "Test",
280
+ });
281
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
282
+ const response = await fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, {
283
+ caBundlePath: serverCerts.caCertPath, // Our self-signed cert as CA
284
+ verifySsl: true,
285
+ });
286
+ expect(response.status).toBe(200);
287
+ const data = (await response.json());
288
+ expect(data.secure).toBe(true);
289
+ });
290
+ test("should REJECT certificates when provided WRONG CA bundle", async () => {
291
+ const tempDir = createTempDir();
292
+ const serverCerts = await generateCertificate(tempDir, "server", {
293
+ cn: "localhost",
294
+ san: ["localhost", "127.0.0.1"],
295
+ org: "Test",
296
+ });
297
+ // Generate a different certificate as the "wrong" CA
298
+ const wrongCaCerts = await generateCertificate(tempDir, "wrong-ca", {
299
+ cn: "wrong-ca",
300
+ san: ["wrong-ca"],
301
+ org: "Wrong CA",
302
+ });
303
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
304
+ // This should fail because the wrong CA can't validate our server certificate
305
+ await expect(fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, {
306
+ caBundlePath: wrongCaCerts.caCertPath,
307
+ verifySsl: true,
308
+ })).rejects.toThrow(); // Should fail with certificate validation error
309
+ });
310
+ test("should REJECT certificates when CA bundle file is corrupted", async () => {
311
+ const tempDir = createTempDir();
312
+ const serverCerts = await generateCertificate(tempDir, "server", {
313
+ cn: "localhost",
314
+ san: ["localhost", "127.0.0.1"],
315
+ org: "Test",
316
+ });
317
+ // Create a corrupted/invalid certificate file
318
+ const corruptedCaPath = path.join(tempDir, "corrupted-ca.pem");
319
+ const corruptedContent = `-----BEGIN CERTIFICATE-----
320
+ This is not a valid certificate content
321
+ Just some random text that looks like a cert
322
+ -----END CERTIFICATE-----`;
323
+ fs.writeFileSync(corruptedCaPath, corruptedContent);
324
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
325
+ // This should fail because the corrupted CA file can't be parsed
326
+ await expect(fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, {
327
+ caBundlePath: corruptedCaPath,
328
+ verifySsl: true,
329
+ })).rejects.toThrow(); // Should fail with certificate parsing or validation error
330
+ });
331
+ test("should REJECT certificates when CA bundle file does not exist", async () => {
332
+ const tempDir = createTempDir();
333
+ const serverCerts = await generateCertificate(tempDir, "server", {
334
+ cn: "localhost",
335
+ san: ["localhost", "127.0.0.1"],
336
+ org: "Test",
337
+ });
338
+ const nonExistentCaPath = path.join(tempDir, "does-not-exist.pem");
339
+ await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
340
+ // This should handle the missing file gracefully but still fail cert validation
341
+ await expect(fetchwithRequestOptions(`https://localhost:${HTTPS_PORT}/secure`, {}, {
342
+ caBundlePath: nonExistentCaPath,
343
+ verifySsl: true,
344
+ })).rejects.toThrow(); // Should fail with certificate validation error
345
+ });
346
+ });
347
+ describe("Error handling", () => {
348
+ test("should handle network errors gracefully", async () => {
349
+ // Try to connect to a non-existent server
350
+ await expect(fetchwithRequestOptions("http://localhost:9999/nonexistent")).rejects.toThrow();
351
+ });
352
+ test("should handle malformed URLs", async () => {
353
+ await expect(fetchwithRequestOptions("not-a-valid-url")).rejects.toThrow();
354
+ });
355
+ });
356
+ });
package/dist/fetch.js CHANGED
@@ -79,8 +79,28 @@ export async function fetchwithRequestOptions(url_, init, requestOptions) {
79
79
  : new HttpProxyAgent(proxy, agentOptions)
80
80
  : new protocol.Agent(agentOptions);
81
81
  let headers = {};
82
- for (const [key, value] of Object.entries(init?.headers || {})) {
83
- headers[key] = value;
82
+ // Handle different header formats
83
+ if (init?.headers) {
84
+ const headersSource = init.headers;
85
+ // Check if it's a Headers-like object (OpenAI v5 HeadersList, standard Headers)
86
+ if (headersSource && typeof headersSource.forEach === "function") {
87
+ // Use forEach method which works reliably on Headers objects
88
+ headersSource.forEach((value, key) => {
89
+ headers[key] = value;
90
+ });
91
+ }
92
+ else if (Array.isArray(headersSource)) {
93
+ // This is an array of [key, value] tuples
94
+ for (const [key, value] of headersSource) {
95
+ headers[key] = value;
96
+ }
97
+ }
98
+ else if (headersSource && typeof headersSource === "object") {
99
+ // This is a plain object
100
+ for (const [key, value] of Object.entries(headersSource)) {
101
+ headers[key] = value;
102
+ }
103
+ }
84
104
  }
85
105
  headers = {
86
106
  ...headers,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,231 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
5
+ import { CertsCache, getCertificateContent } from "./certs.js";
6
+ import { getAgentOptions } from "./getAgentOptions.js";
7
+ // Store original env
8
+ const originalEnv = process.env;
9
+ // Temporary directory for test certificate files
10
+ let tempDir;
11
+ beforeEach(() => {
12
+ // Create a temporary directory for test files
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cert-troubleshoot-test-"));
14
+ process.env = { ...originalEnv };
15
+ });
16
+ afterEach(() => {
17
+ process.env = originalEnv;
18
+ CertsCache.getInstance().clear();
19
+ // Clean up temporary directory
20
+ if (tempDir && fs.existsSync(tempDir)) {
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ }
23
+ });
24
+ describe("SSL certificate handling", () => {
25
+ describe("CA certificate handling", () => {
26
+ test("should handle custom CA bundle from file path", async () => {
27
+ const customCaCert = `-----BEGIN CERTIFICATE-----
28
+ MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
29
+ BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
30
+ aWRnaXRzIFB0eSBMdGQwHhcNMTcwODI4MTkzNDA5WhcNMTgwODI4MTkzNDA5WjBF
31
+ -----END CERTIFICATE-----`;
32
+ const caCertPath = path.join(tempDir, "ca-bundle.pem");
33
+ fs.writeFileSync(caCertPath, customCaCert);
34
+ const options = await getAgentOptions({ caBundlePath: caCertPath });
35
+ expect(options.ca).toBeDefined();
36
+ expect(Array.isArray(options.ca)).toBe(true);
37
+ expect(options.ca).toContain(customCaCert);
38
+ });
39
+ test("should handle NODE_EXTRA_CA_CERTS environment variable", async () => {
40
+ const extraCaCert = `-----BEGIN CERTIFICATE-----
41
+ MIIDUzCCAjugAwIBAgIJALvxFjX5V+/vMA0GCSqGSIb3DQEBCwUAMDgxCzAJBgNV
42
+ BAYTAlVTMRAwDgYDVQQIDAdDb21wYW55MRcwFQYDVQQKDA5Db21wYW55IENvcnAw
43
+ -----END CERTIFICATE-----`;
44
+ const extraCaPath = path.join(tempDir, "extra-ca.crt");
45
+ fs.writeFileSync(extraCaPath, extraCaCert);
46
+ process.env.NODE_EXTRA_CA_CERTS = extraCaPath;
47
+ const options = await getAgentOptions();
48
+ expect(options.ca).toBeDefined();
49
+ expect(Array.isArray(options.ca)).toBe(true);
50
+ expect(options.ca).toContain(extraCaCert);
51
+ });
52
+ test("should combine NODE_EXTRA_CA_CERTS with custom CA bundle", async () => {
53
+ const extraCaCert = `-----BEGIN CERTIFICATE-----
54
+ MIIDExtra1234567890abcdefghijklmnopqrstuvwxyz
55
+ -----END CERTIFICATE-----`;
56
+ const customCaCert = `-----BEGIN CERTIFICATE-----
57
+ MIIDCustom1234567890abcdefghijklmnopqrstuvwxyz
58
+ -----END CERTIFICATE-----`;
59
+ const extraCaPath = path.join(tempDir, "extra-ca.crt");
60
+ const customCaPath = path.join(tempDir, "custom-ca.pem");
61
+ fs.writeFileSync(extraCaPath, extraCaCert);
62
+ fs.writeFileSync(customCaPath, customCaCert);
63
+ process.env.NODE_EXTRA_CA_CERTS = extraCaPath;
64
+ const options = await getAgentOptions({ caBundlePath: customCaPath });
65
+ expect(options.ca).toBeDefined();
66
+ expect(Array.isArray(options.ca)).toBe(true);
67
+ expect(options.ca).toContain(extraCaCert);
68
+ expect(options.ca).toContain(customCaCert);
69
+ });
70
+ });
71
+ describe("Data URI certificate handling", () => {
72
+ test("should handle base64 encoded data URI certificates", () => {
73
+ const originalCert = `-----BEGIN CERTIFICATE-----
74
+ MIIDUzCCAjugAwIBAgIJALvxFjX5V+/vMA0GCSqGSIb3DQEB
75
+ -----END CERTIFICATE-----`;
76
+ const base64Data = Buffer.from(originalCert).toString("base64");
77
+ const dataUri = `data:application/x-pem-file;base64,${base64Data}`;
78
+ const result = getCertificateContent(dataUri);
79
+ expect(result).toBe(originalCert);
80
+ });
81
+ test("should handle URL-encoded data URI certificates", () => {
82
+ const originalCert = "certificate with spaces and special chars: !@#$%^&*()";
83
+ const encodedData = encodeURIComponent(originalCert);
84
+ const dataUri = `data:text/plain,${encodedData}`;
85
+ const result = getCertificateContent(dataUri);
86
+ expect(result).toBe(originalCert);
87
+ });
88
+ });
89
+ describe("SSL/TLS configuration", () => {
90
+ test("should configure SSL verification correctly", async () => {
91
+ // Test with SSL verification enabled
92
+ let options = await getAgentOptions({ verifySsl: true });
93
+ expect(options.rejectUnauthorized).toBe(true);
94
+ // Test with SSL verification disabled
95
+ options = await getAgentOptions({ verifySsl: false });
96
+ expect(options.rejectUnauthorized).toBe(false);
97
+ });
98
+ test("should configure timeout correctly", async () => {
99
+ const customTimeout = 60; // 1 minute in seconds
100
+ const options = await getAgentOptions({ timeout: customTimeout });
101
+ expect(options.timeout).toBe(customTimeout * 1000); // Should be converted to milliseconds
102
+ });
103
+ test("should handle client certificate authentication", async () => {
104
+ const clientCert = `-----BEGIN CERTIFICATE-----
105
+ MIIDClientCert1234567890abcdefghijklmnopqrstuvwxyz
106
+ -----END CERTIFICATE-----`;
107
+ const clientKey = `-----BEGIN PRIVATE KEY-----
108
+ MIIEClientKey567890abcdefghijklmnopqrstuvwxyz
109
+ -----END PRIVATE KEY-----`;
110
+ const clientCertPath = path.join(tempDir, "client.crt");
111
+ const clientKeyPath = path.join(tempDir, "client.key");
112
+ fs.writeFileSync(clientCertPath, clientCert);
113
+ fs.writeFileSync(clientKeyPath, clientKey);
114
+ const options = await getAgentOptions({
115
+ clientCertificate: {
116
+ cert: clientCertPath,
117
+ key: clientKeyPath,
118
+ passphrase: "test-passphrase",
119
+ },
120
+ });
121
+ expect(options.cert).toBe(clientCert);
122
+ expect(options.key).toBe(clientKey);
123
+ expect(options.passphrase).toBe("test-passphrase");
124
+ });
125
+ });
126
+ describe("Certificate caching behavior", () => {
127
+ test("should cache certificate content to avoid repeated file reads", async () => {
128
+ const certsCache = CertsCache.getInstance();
129
+ const certPath = path.join(tempDir, "cached-cert.pem");
130
+ const certContent = `-----BEGIN CERTIFICATE-----
131
+ MIIDCachedCert1234567890abcdefghijklmnopqrstuvwxyz
132
+ -----END CERTIFICATE-----`;
133
+ fs.writeFileSync(certPath, certContent);
134
+ // First call should read from file
135
+ const result1 = await certsCache.getCachedCustomCert(certPath);
136
+ expect(result1).toBe(certContent);
137
+ // Modify the file on disk
138
+ const modifiedContent = certContent.replace("CachedCert", "ModifiedCert");
139
+ fs.writeFileSync(certPath, modifiedContent);
140
+ // Second call should return cached content, not the modified content
141
+ const result2 = await certsCache.getCachedCustomCert(certPath);
142
+ expect(result2).toBe(certContent); // Should still be the original cached content
143
+ });
144
+ test("should handle cache clearing", async () => {
145
+ const certsCache = CertsCache.getInstance();
146
+ const certPath = path.join(tempDir, "clear-cache-cert.pem");
147
+ const originalContent = `-----BEGIN CERTIFICATE-----
148
+ MIIDOriginalCert1234567890abcdefghijklmnopqrstuvwxyz
149
+ -----END CERTIFICATE-----`;
150
+ fs.writeFileSync(certPath, originalContent);
151
+ // Load into cache
152
+ await certsCache.getCachedCustomCert(certPath);
153
+ // Clear cache
154
+ certsCache.clear();
155
+ // Modify file
156
+ const newContent = originalContent.replace("OriginalCert", "NewCert");
157
+ fs.writeFileSync(certPath, newContent);
158
+ // Should read new content after cache clear
159
+ const result2 = await certsCache.getCachedCustomCert(certPath);
160
+ expect(result2).toBe(newContent);
161
+ });
162
+ });
163
+ describe("Error handling scenarios", () => {
164
+ test("should handle non-existent certificate files gracefully", async () => {
165
+ const nonExistentPath = path.join(tempDir, "does-not-exist.pem");
166
+ // Should not throw but should continue with system certificates only
167
+ const options = await getAgentOptions({ caBundlePath: nonExistentPath });
168
+ expect(options.ca).toBeDefined();
169
+ expect(Array.isArray(options.ca)).toBe(true);
170
+ // Should only contain system certificates, not the missing custom one
171
+ expect(options.ca.length).toBeGreaterThan(0);
172
+ });
173
+ test("should handle empty certificate files", () => {
174
+ const emptyCertPath = path.join(tempDir, "empty-cert.pem");
175
+ fs.writeFileSync(emptyCertPath, "");
176
+ const result = getCertificateContent(emptyCertPath);
177
+ expect(result).toBe("");
178
+ });
179
+ test("should handle malformed data URIs", () => {
180
+ const validMalformedDataUris = ["data:invalid-format", "data:"];
181
+ validMalformedDataUris.forEach((uri) => {
182
+ // These should not throw but may return unexpected content
183
+ expect(() => getCertificateContent(uri)).not.toThrow();
184
+ const result = getCertificateContent(uri);
185
+ expect(typeof result).toBe("string");
186
+ });
187
+ // This one will be treated as a file path and should throw
188
+ expect(() => getCertificateContent("not-a-data-uri-at-all")).toThrow("ENOENT");
189
+ });
190
+ });
191
+ describe("Real-world scenarios", () => {
192
+ test("should handle certificate bundle with multiple certificates", () => {
193
+ const bundleContent = `-----BEGIN CERTIFICATE-----
194
+ MIIDRootCA1234567890abcdefghijklmnopqrstuvwxyz
195
+ -----END CERTIFICATE-----
196
+ -----BEGIN CERTIFICATE-----
197
+ MIIDIntermediateCA1234567890abcdefghijklmnopqrstuvwxyz
198
+ -----END CERTIFICATE-----`;
199
+ const bundlePath = path.join(tempDir, "ca-bundle.pem");
200
+ fs.writeFileSync(bundlePath, bundleContent);
201
+ const result = getCertificateContent(bundlePath);
202
+ expect(result).toBe(bundleContent);
203
+ expect(result).toContain("RootCA");
204
+ expect(result).toContain("IntermediateCA");
205
+ });
206
+ test("should work with common certificate file extensions", () => {
207
+ const certExtensions = [".pem", ".crt", ".cer"];
208
+ const testCert = `-----BEGIN CERTIFICATE-----
209
+ MIIDTestCert1234567890abcdefghijklmnopqrstuvwxyz
210
+ -----END CERTIFICATE-----`;
211
+ for (const ext of certExtensions) {
212
+ const certPath = path.join(tempDir, `test-cert${ext}`);
213
+ fs.writeFileSync(certPath, testCert);
214
+ const result = getCertificateContent(certPath);
215
+ expect(result).toBe(testCert);
216
+ }
217
+ });
218
+ test("should handle proxy scenarios with custom certificates", async () => {
219
+ // Test that certificates work properly when proxy might be involved
220
+ const proxyCaCert = `-----BEGIN CERTIFICATE-----
221
+ MIIDProxyCA1234567890abcdefghijklmnopqrstuvwxyz
222
+ -----END CERTIFICATE-----`;
223
+ const proxyCaPath = path.join(tempDir, "proxy-ca.pem");
224
+ fs.writeFileSync(proxyCaPath, proxyCaCert);
225
+ const options = await getAgentOptions({ caBundlePath: proxyCaPath });
226
+ expect(options.ca).toBeDefined();
227
+ expect(options.ca).toContain(proxyCaCert);
228
+ expect(options.keepAlive).toBe(true); // Should work with keep-alive for proxy scenarios
229
+ });
230
+ });
231
+ });
package/dist/stream.js CHANGED
@@ -67,7 +67,13 @@ export function parseDataLine(line) {
67
67
  try {
68
68
  const data = JSON.parse(json);
69
69
  if (data.error) {
70
- throw new Error(`Error streaming response: ${data.error}`);
70
+ if (data.error &&
71
+ typeof data.error === "object" &&
72
+ "message" in data.error) {
73
+ console.error("Error in streamed response:", data.error);
74
+ throw new Error(`Error streaming response: ${data.error.message}`);
75
+ }
76
+ throw new Error(`Error streaming response: ${JSON.stringify(data.error)}`);
71
77
  }
72
78
  return data;
73
79
  }
@@ -69,7 +69,11 @@ describe("parseDataLine", () => {
69
69
  });
70
70
  test("parseDataLine should throw error when data contains error field", () => {
71
71
  const line = 'data: {"error":"something went wrong"}';
72
- expect(() => parseDataLine(line)).toThrow("Error streaming response: something went wrong");
72
+ expect(() => parseDataLine(line)).toThrow('Error streaming response: "something went wrong"');
73
+ });
74
+ test("parseDataLine should throw error when data contains error object with message", () => {
75
+ const line = 'data: {"error":{"message":"detailed error message"}}';
76
+ expect(() => parseDataLine(line)).toThrow("Error streaming response: detailed error message");
73
77
  });
74
78
  test("parseDataLine should handle empty objects", () => {
75
79
  const line = "data: {}";