@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,301 @@
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
+
8
+ // Store original env
9
+ const originalEnv = process.env;
10
+
11
+ // Temporary directory for test certificate files
12
+ let tempDir: string;
13
+
14
+ beforeEach(() => {
15
+ // Create a temporary directory for test files
16
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cert-troubleshoot-test-"));
17
+ process.env = { ...originalEnv };
18
+ });
19
+
20
+ afterEach(() => {
21
+ process.env = originalEnv;
22
+ CertsCache.getInstance().clear();
23
+
24
+ // Clean up temporary directory
25
+ if (tempDir && fs.existsSync(tempDir)) {
26
+ fs.rmSync(tempDir, { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ describe("SSL certificate handling", () => {
31
+ describe("CA certificate handling", () => {
32
+ test("should handle custom CA bundle from file path", async () => {
33
+ const customCaCert = `-----BEGIN CERTIFICATE-----
34
+ MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
35
+ BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
36
+ aWRnaXRzIFB0eSBMdGQwHhcNMTcwODI4MTkzNDA5WhcNMTgwODI4MTkzNDA5WjBF
37
+ -----END CERTIFICATE-----`;
38
+
39
+ const caCertPath = path.join(tempDir, "ca-bundle.pem");
40
+ fs.writeFileSync(caCertPath, customCaCert);
41
+
42
+ const options = await getAgentOptions({ caBundlePath: caCertPath });
43
+
44
+ expect(options.ca).toBeDefined();
45
+ expect(Array.isArray(options.ca)).toBe(true);
46
+ expect(options.ca).toContain(customCaCert);
47
+ });
48
+
49
+ test("should handle NODE_EXTRA_CA_CERTS environment variable", async () => {
50
+ const extraCaCert = `-----BEGIN CERTIFICATE-----
51
+ MIIDUzCCAjugAwIBAgIJALvxFjX5V+/vMA0GCSqGSIb3DQEBCwUAMDgxCzAJBgNV
52
+ BAYTAlVTMRAwDgYDVQQIDAdDb21wYW55MRcwFQYDVQQKDA5Db21wYW55IENvcnAw
53
+ -----END CERTIFICATE-----`;
54
+
55
+ const extraCaPath = path.join(tempDir, "extra-ca.crt");
56
+ fs.writeFileSync(extraCaPath, extraCaCert);
57
+
58
+ process.env.NODE_EXTRA_CA_CERTS = extraCaPath;
59
+
60
+ const options = await getAgentOptions();
61
+
62
+ expect(options.ca).toBeDefined();
63
+ expect(Array.isArray(options.ca)).toBe(true);
64
+ expect(options.ca).toContain(extraCaCert);
65
+ });
66
+
67
+ test("should combine NODE_EXTRA_CA_CERTS with custom CA bundle", async () => {
68
+ const extraCaCert = `-----BEGIN CERTIFICATE-----
69
+ MIIDExtra1234567890abcdefghijklmnopqrstuvwxyz
70
+ -----END CERTIFICATE-----`;
71
+
72
+ const customCaCert = `-----BEGIN CERTIFICATE-----
73
+ MIIDCustom1234567890abcdefghijklmnopqrstuvwxyz
74
+ -----END CERTIFICATE-----`;
75
+
76
+ const extraCaPath = path.join(tempDir, "extra-ca.crt");
77
+ const customCaPath = path.join(tempDir, "custom-ca.pem");
78
+
79
+ fs.writeFileSync(extraCaPath, extraCaCert);
80
+ fs.writeFileSync(customCaPath, customCaCert);
81
+
82
+ process.env.NODE_EXTRA_CA_CERTS = extraCaPath;
83
+
84
+ const options = await getAgentOptions({ caBundlePath: customCaPath });
85
+
86
+ expect(options.ca).toBeDefined();
87
+ expect(Array.isArray(options.ca)).toBe(true);
88
+ expect(options.ca).toContain(extraCaCert);
89
+ expect(options.ca).toContain(customCaCert);
90
+ });
91
+ });
92
+
93
+ describe("Data URI certificate handling", () => {
94
+ test("should handle base64 encoded data URI certificates", () => {
95
+ const originalCert = `-----BEGIN CERTIFICATE-----
96
+ MIIDUzCCAjugAwIBAgIJALvxFjX5V+/vMA0GCSqGSIb3DQEB
97
+ -----END CERTIFICATE-----`;
98
+
99
+ const base64Data = Buffer.from(originalCert).toString("base64");
100
+ const dataUri = `data:application/x-pem-file;base64,${base64Data}`;
101
+
102
+ const result = getCertificateContent(dataUri);
103
+
104
+ expect(result).toBe(originalCert);
105
+ });
106
+
107
+ test("should handle URL-encoded data URI certificates", () => {
108
+ const originalCert =
109
+ "certificate with spaces and special chars: !@#$%^&*()";
110
+ const encodedData = encodeURIComponent(originalCert);
111
+ const dataUri = `data:text/plain,${encodedData}`;
112
+
113
+ const result = getCertificateContent(dataUri);
114
+
115
+ expect(result).toBe(originalCert);
116
+ });
117
+ });
118
+
119
+ describe("SSL/TLS configuration", () => {
120
+ test("should configure SSL verification correctly", async () => {
121
+ // Test with SSL verification enabled
122
+ let options = await getAgentOptions({ verifySsl: true });
123
+ expect(options.rejectUnauthorized).toBe(true);
124
+
125
+ // Test with SSL verification disabled
126
+ options = await getAgentOptions({ verifySsl: false });
127
+ expect(options.rejectUnauthorized).toBe(false);
128
+ });
129
+
130
+ test("should configure timeout correctly", async () => {
131
+ const customTimeout = 60; // 1 minute in seconds
132
+ const options = await getAgentOptions({ timeout: customTimeout });
133
+
134
+ expect(options.timeout).toBe(customTimeout * 1000); // Should be converted to milliseconds
135
+ });
136
+
137
+ test("should handle client certificate authentication", async () => {
138
+ const clientCert = `-----BEGIN CERTIFICATE-----
139
+ MIIDClientCert1234567890abcdefghijklmnopqrstuvwxyz
140
+ -----END CERTIFICATE-----`;
141
+
142
+ const clientKey = `-----BEGIN PRIVATE KEY-----
143
+ MIIEClientKey567890abcdefghijklmnopqrstuvwxyz
144
+ -----END PRIVATE KEY-----`;
145
+
146
+ const clientCertPath = path.join(tempDir, "client.crt");
147
+ const clientKeyPath = path.join(tempDir, "client.key");
148
+
149
+ fs.writeFileSync(clientCertPath, clientCert);
150
+ fs.writeFileSync(clientKeyPath, clientKey);
151
+
152
+ const options = await getAgentOptions({
153
+ clientCertificate: {
154
+ cert: clientCertPath,
155
+ key: clientKeyPath,
156
+ passphrase: "test-passphrase",
157
+ },
158
+ });
159
+
160
+ expect(options.cert).toBe(clientCert);
161
+ expect(options.key).toBe(clientKey);
162
+ expect(options.passphrase).toBe("test-passphrase");
163
+ });
164
+ });
165
+
166
+ describe("Certificate caching behavior", () => {
167
+ test("should cache certificate content to avoid repeated file reads", async () => {
168
+ const certsCache = CertsCache.getInstance();
169
+ const certPath = path.join(tempDir, "cached-cert.pem");
170
+ const certContent = `-----BEGIN CERTIFICATE-----
171
+ MIIDCachedCert1234567890abcdefghijklmnopqrstuvwxyz
172
+ -----END CERTIFICATE-----`;
173
+
174
+ fs.writeFileSync(certPath, certContent);
175
+
176
+ // First call should read from file
177
+ const result1 = await certsCache.getCachedCustomCert(certPath);
178
+ expect(result1).toBe(certContent);
179
+
180
+ // Modify the file on disk
181
+ const modifiedContent = certContent.replace("CachedCert", "ModifiedCert");
182
+ fs.writeFileSync(certPath, modifiedContent);
183
+
184
+ // Second call should return cached content, not the modified content
185
+ const result2 = await certsCache.getCachedCustomCert(certPath);
186
+ expect(result2).toBe(certContent); // Should still be the original cached content
187
+ });
188
+
189
+ test("should handle cache clearing", async () => {
190
+ const certsCache = CertsCache.getInstance();
191
+ const certPath = path.join(tempDir, "clear-cache-cert.pem");
192
+ const originalContent = `-----BEGIN CERTIFICATE-----
193
+ MIIDOriginalCert1234567890abcdefghijklmnopqrstuvwxyz
194
+ -----END CERTIFICATE-----`;
195
+
196
+ fs.writeFileSync(certPath, originalContent);
197
+
198
+ // Load into cache
199
+ await certsCache.getCachedCustomCert(certPath);
200
+
201
+ // Clear cache
202
+ certsCache.clear();
203
+
204
+ // Modify file
205
+ const newContent = originalContent.replace("OriginalCert", "NewCert");
206
+ fs.writeFileSync(certPath, newContent);
207
+
208
+ // Should read new content after cache clear
209
+ const result2 = await certsCache.getCachedCustomCert(certPath);
210
+ expect(result2).toBe(newContent);
211
+ });
212
+ });
213
+
214
+ describe("Error handling scenarios", () => {
215
+ test("should handle non-existent certificate files gracefully", async () => {
216
+ const nonExistentPath = path.join(tempDir, "does-not-exist.pem");
217
+
218
+ // Should not throw but should continue with system certificates only
219
+ const options = await getAgentOptions({ caBundlePath: nonExistentPath });
220
+
221
+ expect(options.ca).toBeDefined();
222
+ expect(Array.isArray(options.ca)).toBe(true);
223
+ // Should only contain system certificates, not the missing custom one
224
+ expect(options.ca.length).toBeGreaterThan(0);
225
+ });
226
+
227
+ test("should handle empty certificate files", () => {
228
+ const emptyCertPath = path.join(tempDir, "empty-cert.pem");
229
+ fs.writeFileSync(emptyCertPath, "");
230
+
231
+ const result = getCertificateContent(emptyCertPath);
232
+ expect(result).toBe("");
233
+ });
234
+
235
+ test("should handle malformed data URIs", () => {
236
+ const validMalformedDataUris = ["data:invalid-format", "data:"];
237
+
238
+ validMalformedDataUris.forEach((uri) => {
239
+ // These should not throw but may return unexpected content
240
+ expect(() => getCertificateContent(uri)).not.toThrow();
241
+ const result = getCertificateContent(uri);
242
+ expect(typeof result).toBe("string");
243
+ });
244
+
245
+ // This one will be treated as a file path and should throw
246
+ expect(() => getCertificateContent("not-a-data-uri-at-all")).toThrow(
247
+ "ENOENT",
248
+ );
249
+ });
250
+ });
251
+
252
+ describe("Real-world scenarios", () => {
253
+ test("should handle certificate bundle with multiple certificates", () => {
254
+ const bundleContent = `-----BEGIN CERTIFICATE-----
255
+ MIIDRootCA1234567890abcdefghijklmnopqrstuvwxyz
256
+ -----END CERTIFICATE-----
257
+ -----BEGIN CERTIFICATE-----
258
+ MIIDIntermediateCA1234567890abcdefghijklmnopqrstuvwxyz
259
+ -----END CERTIFICATE-----`;
260
+
261
+ const bundlePath = path.join(tempDir, "ca-bundle.pem");
262
+ fs.writeFileSync(bundlePath, bundleContent);
263
+
264
+ const result = getCertificateContent(bundlePath);
265
+ expect(result).toBe(bundleContent);
266
+ expect(result).toContain("RootCA");
267
+ expect(result).toContain("IntermediateCA");
268
+ });
269
+
270
+ test("should work with common certificate file extensions", () => {
271
+ const certExtensions = [".pem", ".crt", ".cer"];
272
+ const testCert = `-----BEGIN CERTIFICATE-----
273
+ MIIDTestCert1234567890abcdefghijklmnopqrstuvwxyz
274
+ -----END CERTIFICATE-----`;
275
+
276
+ for (const ext of certExtensions) {
277
+ const certPath = path.join(tempDir, `test-cert${ext}`);
278
+ fs.writeFileSync(certPath, testCert);
279
+
280
+ const result = getCertificateContent(certPath);
281
+ expect(result).toBe(testCert);
282
+ }
283
+ });
284
+
285
+ test("should handle proxy scenarios with custom certificates", async () => {
286
+ // Test that certificates work properly when proxy might be involved
287
+ const proxyCaCert = `-----BEGIN CERTIFICATE-----
288
+ MIIDProxyCA1234567890abcdefghijklmnopqrstuvwxyz
289
+ -----END CERTIFICATE-----`;
290
+
291
+ const proxyCaPath = path.join(tempDir, "proxy-ca.pem");
292
+ fs.writeFileSync(proxyCaPath, proxyCaCert);
293
+
294
+ const options = await getAgentOptions({ caBundlePath: proxyCaPath });
295
+
296
+ expect(options.ca).toBeDefined();
297
+ expect(options.ca).toContain(proxyCaCert);
298
+ expect(options.keepAlive).toBe(true); // Should work with keep-alive for proxy scenarios
299
+ });
300
+ });
301
+ });
@@ -86,7 +86,14 @@ describe("parseDataLine", () => {
86
86
  test("parseDataLine should throw error when data contains error field", () => {
87
87
  const line = 'data: {"error":"something went wrong"}';
88
88
  expect(() => parseDataLine(line)).toThrow(
89
- "Error streaming response: something went wrong",
89
+ 'Error streaming response: "something went wrong"',
90
+ );
91
+ });
92
+
93
+ test("parseDataLine should throw error when data contains error object with message", () => {
94
+ const line = 'data: {"error":{"message":"detailed error message"}}';
95
+ expect(() => parseDataLine(line)).toThrow(
96
+ "Error streaming response: detailed error message",
90
97
  );
91
98
  });
92
99
 
package/src/stream.ts CHANGED
@@ -81,7 +81,17 @@ export function parseDataLine(line: string): any {
81
81
  try {
82
82
  const data = JSON.parse(json);
83
83
  if (data.error) {
84
- throw new Error(`Error streaming response: ${data.error}`);
84
+ if (
85
+ data.error &&
86
+ typeof data.error === "object" &&
87
+ "message" in data.error
88
+ ) {
89
+ console.error("Error in streamed response:", data.error);
90
+ throw new Error(`Error streaming response: ${data.error.message}`);
91
+ }
92
+ throw new Error(
93
+ `Error streaming response: ${JSON.stringify(data.error)}`,
94
+ );
85
95
  }
86
96
 
87
97
  return data;
package/vitest.config.ts CHANGED
@@ -5,6 +5,7 @@ export default defineConfig({
5
5
  environment: "node",
6
6
  timeout: 10000,
7
7
  globals: true,
8
+ include: ["**/*.{test,spec}.?(c|m)[jt]s?(x)"],
8
9
  },
9
10
  resolve: {
10
11
  alias: {
@@ -12,6 +13,8 @@ export default defineConfig({
12
13
  "./getAgentOptions.js": "./getAgentOptions.ts",
13
14
  "./stream.js": "./stream.ts",
14
15
  "./util.js": "./util.ts",
16
+ "./certs.js": "./certs.ts",
17
+ "./fetch.js": "./fetch.ts",
15
18
  },
16
19
  },
17
20
  });
@@ -1,130 +0,0 @@
1
- import { globalAgent } from "https";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { getAgentOptions } from "./getAgentOptions.js";
6
- // Store original env
7
- const originalEnv = process.env;
8
- const originalGlobalAgentOptions = { ...globalAgent.options };
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(), "fetch-test-"));
14
- process.env = { ...originalEnv };
15
- // Reset globalAgent for each test
16
- globalAgent.options = { ...originalGlobalAgentOptions };
17
- });
18
- afterEach(() => {
19
- process.env = originalEnv;
20
- globalAgent.options = originalGlobalAgentOptions;
21
- // Clean up temporary directory
22
- try {
23
- fs.rmSync(tempDir, { recursive: true, force: true });
24
- }
25
- catch (error) {
26
- console.error(`Failed to remove temp directory: ${error}`);
27
- }
28
- });
29
- // Helper function to create test certificate files
30
- function createTestCertFile(filename, content) {
31
- const filePath = path.join(tempDir, filename);
32
- fs.writeFileSync(filePath, content, "utf8");
33
- return filePath;
34
- }
35
- test("getAgentOptions returns basic configuration with default values", () => {
36
- const options = getAgentOptions();
37
- // Check default timeout (7200 seconds = 2 hours = 7,200,000 ms)
38
- expect(options.timeout).toBe(7200000);
39
- expect(options.sessionTimeout).toBe(7200000);
40
- expect(options.keepAliveMsecs).toBe(7200000);
41
- expect(options.keepAlive).toBe(true);
42
- // Verify certificates array exists and contains items
43
- expect(options.ca).toBeInstanceOf(Array);
44
- expect(options.ca.length).toBeGreaterThan(0);
45
- // Verify at least one of the real TLS root certificates is included
46
- // This assumes there's at least one certificate with "CERTIFICATE" in it
47
- expect(options.ca.some((cert) => cert.includes("CERTIFICATE"))).toBe(true);
48
- });
49
- test("getAgentOptions respects custom timeout", () => {
50
- const customTimeout = 300; // 5 minutes
51
- const options = getAgentOptions({ timeout: customTimeout });
52
- // Check timeout values (300 seconds = 300,000 ms)
53
- expect(options.timeout).toBe(300000);
54
- expect(options.sessionTimeout).toBe(300000);
55
- expect(options.keepAliveMsecs).toBe(300000);
56
- });
57
- test("getAgentOptions uses verifySsl setting", () => {
58
- // With verifySsl true
59
- let options = getAgentOptions({ verifySsl: true });
60
- expect(options.rejectUnauthorized).toBe(true);
61
- // With verifySsl false
62
- options = getAgentOptions({ verifySsl: false });
63
- expect(options.rejectUnauthorized).toBe(false);
64
- });
65
- test("getAgentOptions incorporates custom CA bundle paths", () => {
66
- // Create a test CA bundle file
67
- const caBundleContent = "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIJAMcuSp7chAYdMA==\n-----END CERTIFICATE-----";
68
- const caBundlePath = createTestCertFile("ca-bundle.pem", caBundleContent);
69
- // Single string path
70
- let options = getAgentOptions({ caBundlePath });
71
- // Verify that our test certificate is included in the CA list
72
- expect(options.ca).toContain(caBundleContent);
73
- // Create multiple test CA bundle files
74
- const caContent1 = "-----BEGIN CERTIFICATE-----\nABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n-----END CERTIFICATE-----";
75
- const caContent2 = "-----BEGIN CERTIFICATE-----\n0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\n-----END CERTIFICATE-----";
76
- const caPath1 = createTestCertFile("ca1.pem", caContent1);
77
- const caPath2 = createTestCertFile("ca2.pem", caContent2);
78
- // Array of paths
79
- options = getAgentOptions({
80
- caBundlePath: [caPath1, caPath2],
81
- });
82
- // Verify that both test certificates are included in the CA list
83
- expect(options.ca).toContain(caContent1);
84
- expect(options.ca).toContain(caContent2);
85
- });
86
- test("getAgentOptions includes global certs when running as binary", () => {
87
- // Set up test certs in globalAgent
88
- globalAgent.options.ca = ["global-cert-1", "global-cert-2"];
89
- // Set IS_BINARY environment variable
90
- process.env.IS_BINARY = "true";
91
- const options = getAgentOptions();
92
- // Test for global certs
93
- expect(options.ca).toContain("global-cert-1");
94
- expect(options.ca).toContain("global-cert-2");
95
- });
96
- test("getAgentOptions handles client certificate configuration", () => {
97
- // Create test certificate files
98
- const clientCertContent = "-----BEGIN CERTIFICATE-----\nCLIENTCERT\n-----END CERTIFICATE-----";
99
- const clientKeyContent = "-----BEGIN PRIVATE KEY-----\nCLIENTKEY\n-----END PRIVATE KEY-----";
100
- const certPath = createTestCertFile("client.cert", clientCertContent);
101
- const keyPath = createTestCertFile("client.key", clientKeyContent);
102
- const clientCertOptions = {
103
- clientCertificate: {
104
- cert: certPath,
105
- key: keyPath,
106
- passphrase: "secret-passphrase",
107
- },
108
- };
109
- const options = getAgentOptions(clientCertOptions);
110
- expect(options.cert).toBe(clientCertContent);
111
- expect(options.key).toBe(clientKeyContent);
112
- expect(options.passphrase).toBe("secret-passphrase");
113
- });
114
- test("getAgentOptions handles client certificate without passphrase", () => {
115
- // Create test certificate files
116
- const clientCertContent = "-----BEGIN CERTIFICATE-----\nCLIENTCERT2\n-----END CERTIFICATE-----";
117
- const clientKeyContent = "-----BEGIN PRIVATE KEY-----\nCLIENTKEY2\n-----END PRIVATE KEY-----";
118
- const certPath = createTestCertFile("client2.cert", clientCertContent);
119
- const keyPath = createTestCertFile("client2.key", clientKeyContent);
120
- const clientCertOptions = {
121
- clientCertificate: {
122
- cert: certPath,
123
- key: keyPath,
124
- },
125
- };
126
- const options = getAgentOptions(clientCertOptions);
127
- expect(options.cert).toBe(clientCertContent);
128
- expect(options.key).toBe(clientKeyContent);
129
- expect(options.passphrase).toBeUndefined();
130
- });
@@ -1 +0,0 @@
1
- {"version":"3.2.0","results":[[":node-fetch-bug.test.ts",{"duration":10.538165999999933,"failed":true}],[":boundary-assumption.test.ts",{"duration":392.1355840000324,"failed":false}],[":chunk-transformer.test.ts",{"duration":0,"failed":true}],[":fetch.test.ts",{"duration":115.30825000000186,"failed":false}],[":getAgentOptions.test.ts",{"duration":6.5474579999936395,"failed":false}],[":stream.test.ts",{"duration":68.50500000000466,"failed":false}],[":premature-close.test.ts",{"duration":5329.249708,"failed":true}],[":util.test.ts",{"duration":3.016583999999966,"failed":false}]]}
@@ -1 +0,0 @@
1
- {"version":"3.2.1","results":[[":node-fetch-patch.test.ts",{"duration":125.8410000000149,"failed":true}],[":remove/node-fetch-patch.test.ts",{"duration":214.92558300006203,"failed":true}],[":patch-test.test.js",{"duration":1.4479579999999999,"failed":false}],[":premature-close.test.ts",{"duration":5396.810541999992,"failed":true}],[":util.test.ts",{"duration":4.095832999795675,"failed":false}]]}
File without changes