@continuedev/fetch 1.0.12 → 1.0.14
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.
- package/dist/certs.d.ts +17 -0
- package/dist/certs.js +105 -0
- package/dist/certs.test.d.ts +1 -0
- package/dist/certs.test.js +139 -0
- package/dist/fetch.js +6 -9
- package/dist/getAgentOptions.d.ts +2 -2
- package/dist/getAgentOptions.js +17 -24
- package/dist/getAgentOptions.test.d.ts +1 -0
- package/dist/getAgentOptions.test.js +141 -0
- package/dist/node-fetch-patch.d.ts +16 -0
- package/dist/node-fetch-patch.js +395 -0
- package/dist/node-fetch-patch.test.d.ts +1 -0
- package/dist/node-fetch-patch.test.js +50 -0
- package/dist/stream.js +27 -4
- package/dist/stream.test.js +1 -0
- package/dist/util.d.ts +6 -1
- package/dist/util.js +57 -17
- package/dist/util.test.js +94 -20
- package/package.json +3 -7
- package/src/certs.test.ts +187 -0
- package/src/certs.ts +129 -0
- package/src/fetch.ts +6 -10
- package/src/{fetch.test.ts → getAgentOptions.test.ts} +33 -16
- package/src/getAgentOptions.ts +29 -35
- package/src/node-fetch-patch.js +518 -0
- package/src/node-fetch-patch.test.js +67 -0
- package/src/node_modules/.vite/vitest/d41d8cd98f00b204e9800998ecf8427e/results.json +1 -0
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/stream.test.ts +1 -0
- package/src/stream.ts +29 -4
- package/src/util.test.ts +132 -20
- package/src/util.ts +84 -19
- package/tsconfig.json +2 -2
- package/vitest.config.ts +17 -0
- package/jest.config.mjs +0 -20
- package/jest.globals.d.ts +0 -14
package/dist/util.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
+
import { getProxyFromEnv, patternMatchesHostname, shouldBypassProxy, } from "./util.js";
|
|
3
3
|
// Reset environment variables after each test
|
|
4
4
|
afterEach(() => {
|
|
5
|
-
|
|
5
|
+
vi.resetModules();
|
|
6
6
|
process.env = {};
|
|
7
7
|
});
|
|
8
8
|
// Tests for getProxyFromEnv
|
|
@@ -42,44 +42,118 @@ test("getProxyFromEnv prefers HTTPS_PROXY over other env vars for https protocol
|
|
|
42
42
|
process.env.http_proxy = "http://notused3.example.com";
|
|
43
43
|
expect(getProxyFromEnv("https:")).toBe("https://preferred.example.com");
|
|
44
44
|
});
|
|
45
|
+
// Tests for patternMatchesHostname
|
|
46
|
+
test("patternMatchesHostname with exact hostname match", () => {
|
|
47
|
+
expect(patternMatchesHostname("example.com", "example.com")).toBe(true);
|
|
48
|
+
expect(patternMatchesHostname("example.com", "different.com")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
test("patternMatchesHostname with wildcard domains", () => {
|
|
51
|
+
expect(patternMatchesHostname("sub.example.com", "*.example.com")).toBe(true);
|
|
52
|
+
expect(patternMatchesHostname("sub.sub.example.com", "*.example.com")).toBe(true);
|
|
53
|
+
expect(patternMatchesHostname("example.com", "*.example.com")).toBe(false);
|
|
54
|
+
expect(patternMatchesHostname("sub.different.com", "*.example.com")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
test("patternMatchesHostname with domain suffix", () => {
|
|
57
|
+
expect(patternMatchesHostname("sub.example.com", ".example.com")).toBe(true);
|
|
58
|
+
expect(patternMatchesHostname("example.com", ".example.com")).toBe(true);
|
|
59
|
+
expect(patternMatchesHostname("different.com", ".example.com")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
test("patternMatchesHostname with case insensitivity", () => {
|
|
62
|
+
expect(patternMatchesHostname("EXAMPLE.com", "example.COM")).toBe(true);
|
|
63
|
+
expect(patternMatchesHostname("sub.EXAMPLE.com", "*.example.COM")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
// Port handling tests
|
|
66
|
+
test("patternMatchesHostname with exact port match", () => {
|
|
67
|
+
expect(patternMatchesHostname("example.com:8080", "example.com:8080")).toBe(true);
|
|
68
|
+
expect(patternMatchesHostname("example.com:8080", "example.com:9090")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
test("patternMatchesHostname with port in pattern but not in hostname", () => {
|
|
71
|
+
expect(patternMatchesHostname("example.com", "example.com:8080")).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
test("patternMatchesHostname with port in hostname but not in pattern", () => {
|
|
74
|
+
expect(patternMatchesHostname("example.com:8080", "example.com")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
test("patternMatchesHostname with wildcard domains and ports", () => {
|
|
77
|
+
expect(patternMatchesHostname("sub.example.com:8080", "*.example.com:8080")).toBe(true);
|
|
78
|
+
expect(patternMatchesHostname("sub.example.com:9090", "*.example.com:8080")).toBe(false);
|
|
79
|
+
expect(patternMatchesHostname("sub.example.com", "*.example.com:8080")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
test("patternMatchesHostname with domain suffix and ports", () => {
|
|
82
|
+
expect(patternMatchesHostname("sub.example.com:8080", ".example.com:8080")).toBe(true);
|
|
83
|
+
expect(patternMatchesHostname("example.com:8080", ".example.com:8080")).toBe(true);
|
|
84
|
+
expect(patternMatchesHostname("sub.example.com:9090", ".example.com:8080")).toBe(false);
|
|
85
|
+
});
|
|
45
86
|
// Tests for shouldBypassProxy
|
|
46
87
|
test("shouldBypassProxy returns false when NO_PROXY is not set", () => {
|
|
47
|
-
expect(shouldBypassProxy("example.com")).toBe(false);
|
|
88
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(false);
|
|
48
89
|
});
|
|
49
90
|
test("shouldBypassProxy returns true for exact hostname match", () => {
|
|
50
91
|
process.env.NO_PROXY = "example.com,another.com";
|
|
51
|
-
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
92
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(true);
|
|
52
93
|
});
|
|
53
94
|
test("shouldBypassProxy returns false when hostname doesn't match any NO_PROXY entry", () => {
|
|
54
95
|
process.env.NO_PROXY = "example.com,another.com";
|
|
55
|
-
expect(shouldBypassProxy("different.com")).toBe(false);
|
|
96
|
+
expect(shouldBypassProxy("different.com", undefined)).toBe(false);
|
|
56
97
|
});
|
|
57
98
|
test("shouldBypassProxy handles lowercase no_proxy", () => {
|
|
58
99
|
process.env.no_proxy = "example.com";
|
|
59
|
-
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
100
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(true);
|
|
60
101
|
});
|
|
61
102
|
test("shouldBypassProxy works with wildcard domains", () => {
|
|
62
103
|
process.env.NO_PROXY = "*.example.com";
|
|
63
|
-
expect(shouldBypassProxy("sub.example.com")).toBe(true);
|
|
64
|
-
expect(shouldBypassProxy("example.com")).toBe(false);
|
|
65
|
-
expect(shouldBypassProxy("different.com")).toBe(false);
|
|
104
|
+
expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
|
|
105
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(false);
|
|
106
|
+
expect(shouldBypassProxy("different.com", undefined)).toBe(false);
|
|
66
107
|
});
|
|
67
108
|
test("shouldBypassProxy works with domain suffix", () => {
|
|
68
109
|
process.env.NO_PROXY = ".example.com";
|
|
69
|
-
expect(shouldBypassProxy("sub.example.com")).toBe(true);
|
|
70
|
-
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
71
|
-
expect(shouldBypassProxy("different.com")).toBe(false);
|
|
110
|
+
expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
|
|
111
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(true);
|
|
112
|
+
expect(shouldBypassProxy("different.com", undefined)).toBe(false);
|
|
72
113
|
});
|
|
73
114
|
test("shouldBypassProxy handles multiple entries with different patterns", () => {
|
|
74
115
|
process.env.NO_PROXY = "internal.local,*.example.com,.test.com";
|
|
75
|
-
expect(shouldBypassProxy("internal.local")).toBe(true);
|
|
76
|
-
expect(shouldBypassProxy("sub.example.com")).toBe(true);
|
|
77
|
-
expect(shouldBypassProxy("sub.test.com")).toBe(true);
|
|
78
|
-
expect(shouldBypassProxy("test.com")).toBe(true);
|
|
79
|
-
expect(shouldBypassProxy("example.org")).toBe(false);
|
|
116
|
+
expect(shouldBypassProxy("internal.local", undefined)).toBe(true);
|
|
117
|
+
expect(shouldBypassProxy("sub.example.com", undefined)).toBe(true);
|
|
118
|
+
expect(shouldBypassProxy("sub.test.com", undefined)).toBe(true);
|
|
119
|
+
expect(shouldBypassProxy("test.com", undefined)).toBe(true);
|
|
120
|
+
expect(shouldBypassProxy("example.org", undefined)).toBe(false);
|
|
80
121
|
});
|
|
81
122
|
test("shouldBypassProxy ignores whitespace in NO_PROXY", () => {
|
|
82
123
|
process.env.NO_PROXY = " example.com , *.test.org ";
|
|
83
|
-
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
84
|
-
expect(shouldBypassProxy("subdomain.test.org")).toBe(true);
|
|
124
|
+
expect(shouldBypassProxy("example.com", undefined)).toBe(true);
|
|
125
|
+
expect(shouldBypassProxy("subdomain.test.org", undefined)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
test("shouldBypassProxy with ports in NO_PROXY", () => {
|
|
128
|
+
process.env.NO_PROXY = "example.com:8080,*.test.org:443,.internal.net:8443";
|
|
129
|
+
expect(shouldBypassProxy("example.com:8080", undefined)).toBe(true);
|
|
130
|
+
expect(shouldBypassProxy("example.com:9090", undefined)).toBe(false);
|
|
131
|
+
expect(shouldBypassProxy("sub.test.org:443", undefined)).toBe(true);
|
|
132
|
+
expect(shouldBypassProxy("sub.internal.net:8443", undefined)).toBe(true);
|
|
133
|
+
expect(shouldBypassProxy("internal.net:8443", undefined)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
test("shouldBypassProxy accepts options with noProxy patterns", () => {
|
|
136
|
+
const options = { noProxy: ["example.com:8080", "*.internal.net"] };
|
|
137
|
+
expect(shouldBypassProxy("example.com:8080", options)).toBe(true);
|
|
138
|
+
expect(shouldBypassProxy("example.com", options)).toBe(false);
|
|
139
|
+
expect(shouldBypassProxy("server.internal.net", options)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
test("shouldBypassProxy combines environment and options noProxy patterns", () => {
|
|
142
|
+
process.env.NO_PROXY = "example.org,*.test.com";
|
|
143
|
+
const options = { noProxy: ["example.com:8080", "*.internal.net"] };
|
|
144
|
+
expect(shouldBypassProxy("example.org", options)).toBe(true);
|
|
145
|
+
expect(shouldBypassProxy("sub.test.com", options)).toBe(true);
|
|
146
|
+
expect(shouldBypassProxy("example.com:8080", options)).toBe(true);
|
|
147
|
+
expect(shouldBypassProxy("server.internal.net", options)).toBe(true);
|
|
148
|
+
expect(shouldBypassProxy("other.domain", options)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
test("shouldBypassProxy handles empty noProxy array in options", () => {
|
|
151
|
+
process.env.NO_PROXY = "example.org";
|
|
152
|
+
const options = { noProxy: [] };
|
|
153
|
+
expect(shouldBypassProxy("example.org", options)).toBe(true);
|
|
154
|
+
expect(shouldBypassProxy("different.com", options)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
test("shouldBypassProxy handles undefined options", () => {
|
|
157
|
+
process.env.NO_PROXY = "example.org";
|
|
158
|
+
expect(shouldBypassProxy("example.org", undefined)).toBe(true);
|
|
85
159
|
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuedev/fetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "
|
|
9
|
+
"test": "vitest run",
|
|
10
10
|
"build": "tsc"
|
|
11
11
|
},
|
|
12
12
|
"author": "Nate Sesti and Ty Dunn",
|
|
@@ -20,10 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/follow-redirects": "^1.14.4",
|
|
23
|
-
"
|
|
24
|
-
"cross-env": "^7.0.3",
|
|
25
|
-
"jest": "^29.7.0",
|
|
26
|
-
"ts-jest": "^29.3.4",
|
|
27
|
-
"ts-node": "^10.9.2"
|
|
23
|
+
"vitest": "^3.2.0"
|
|
28
24
|
}
|
|
29
25
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { beforeEach, expect, test, vi } from "vitest";
|
|
3
|
+
import { CertsCache, getCertificateContent } from "./certs.js";
|
|
4
|
+
|
|
5
|
+
// Mock fs module
|
|
6
|
+
vi.mock("node:fs", () => ({
|
|
7
|
+
readFileSync: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const mockReadFileSync = vi.mocked(fs.readFileSync);
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("getCertificateContent should decode base64 data URI correctly", () => {
|
|
17
|
+
const testCert =
|
|
18
|
+
"-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----";
|
|
19
|
+
const base64Data = Buffer.from(testCert, "utf8").toString("base64");
|
|
20
|
+
const dataUri = `data:application/x-pem-file;base64,${base64Data}`;
|
|
21
|
+
|
|
22
|
+
const result = getCertificateContent(dataUri);
|
|
23
|
+
|
|
24
|
+
expect(result).toBe(testCert);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("getCertificateContent should decode URL-encoded data URI correctly", () => {
|
|
28
|
+
const testCert =
|
|
29
|
+
"-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----";
|
|
30
|
+
const encodedData = encodeURIComponent(testCert);
|
|
31
|
+
const dataUri = `data:text/plain,${encodedData}`;
|
|
32
|
+
|
|
33
|
+
const result = getCertificateContent(dataUri);
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(testCert);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("getCertificateContent should handle plain text data URI correctly", () => {
|
|
39
|
+
const testCert = "simple-cert-content";
|
|
40
|
+
const dataUri = `data:text/plain,${testCert}`;
|
|
41
|
+
|
|
42
|
+
const result = getCertificateContent(dataUri);
|
|
43
|
+
|
|
44
|
+
expect(result).toBe(testCert);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("getCertificateContent should read file when input is a file path", () => {
|
|
48
|
+
const filePath = "/path/to/cert.pem";
|
|
49
|
+
const expectedContent =
|
|
50
|
+
"-----BEGIN CERTIFICATE-----\nfile content\n-----END CERTIFICATE-----";
|
|
51
|
+
|
|
52
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
53
|
+
|
|
54
|
+
const result = getCertificateContent(filePath);
|
|
55
|
+
|
|
56
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
57
|
+
expect(result).toBe(expectedContent);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("getCertificateContent should handle data URI with different media types", () => {
|
|
61
|
+
const testData = "certificate-data";
|
|
62
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
63
|
+
const dataUri = `data:application/x-x509-ca-cert;base64,${base64Data}`;
|
|
64
|
+
|
|
65
|
+
const result = getCertificateContent(dataUri);
|
|
66
|
+
|
|
67
|
+
expect(result).toBe(testData);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("getCertificateContent should handle data URI without media type", () => {
|
|
71
|
+
const testData = "simple-data";
|
|
72
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
73
|
+
const dataUri = `data:;base64,${base64Data}`;
|
|
74
|
+
|
|
75
|
+
const result = getCertificateContent(dataUri);
|
|
76
|
+
|
|
77
|
+
expect(result).toBe(testData);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("getCertificateContent should handle data URI without media type or encoding", () => {
|
|
81
|
+
const testData = "simple-data";
|
|
82
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
83
|
+
const dataUri = `data:;base64,${base64Data}`;
|
|
84
|
+
|
|
85
|
+
const result = getCertificateContent(dataUri);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(testData);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("getCertificateContent should handle relative file paths", () => {
|
|
91
|
+
const filePath = "./certs/ca.pem";
|
|
92
|
+
const expectedContent = "certificate from relative path";
|
|
93
|
+
|
|
94
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
95
|
+
|
|
96
|
+
const result = getCertificateContent(filePath);
|
|
97
|
+
|
|
98
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
99
|
+
expect(result).toBe(expectedContent);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("getCertificateContent should handle data URI with special characters in URL encoding", () => {
|
|
103
|
+
const testCert = "cert with spaces and special chars: !@#$%";
|
|
104
|
+
const encodedData = encodeURIComponent(testCert);
|
|
105
|
+
const dataUri = `data:text/plain,${encodedData}`;
|
|
106
|
+
|
|
107
|
+
const result = getCertificateContent(dataUri);
|
|
108
|
+
|
|
109
|
+
expect(result).toBe(testCert);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("CertsCache.getCachedCustomCert should cache and return certificate content", async () => {
|
|
113
|
+
const certsCache = CertsCache.getInstance();
|
|
114
|
+
const filePath = "/path/to/custom/cert.pem";
|
|
115
|
+
const expectedContent = "custom cert content";
|
|
116
|
+
|
|
117
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
118
|
+
|
|
119
|
+
const cert1 = await certsCache.getCachedCustomCert(filePath);
|
|
120
|
+
expect(cert1).toBe(expectedContent);
|
|
121
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
123
|
+
|
|
124
|
+
// Call again to check if it's cached
|
|
125
|
+
const cert2 = await certsCache.getCachedCustomCert(filePath);
|
|
126
|
+
expect(cert2).toBe(expectedContent);
|
|
127
|
+
// readFileSync should not be called again
|
|
128
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("CertsCache.getAllCachedCustomCerts should return all cached custom certs", async () => {
|
|
132
|
+
const certsCache = CertsCache.getInstance();
|
|
133
|
+
const filePaths = ["/path/to/cert1.pem", "/path/to/cert2.pem"];
|
|
134
|
+
const expectedContent1 = "content of cert1";
|
|
135
|
+
const expectedContent2 = "content of cert2";
|
|
136
|
+
|
|
137
|
+
mockReadFileSync.mockReturnValueOnce(expectedContent1);
|
|
138
|
+
mockReadFileSync.mockReturnValueOnce(expectedContent2);
|
|
139
|
+
|
|
140
|
+
const certs = await certsCache.getAllCachedCustomCerts(filePaths);
|
|
141
|
+
expect(certs).toEqual([expectedContent1, expectedContent2]);
|
|
142
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("CertsCache.getCa should return combined CA when caBundlePath is provided", async () => {
|
|
146
|
+
const certsCache = CertsCache.getInstance();
|
|
147
|
+
const fixedCa = ["fixed CA cert"];
|
|
148
|
+
const customCertPath = "/path/to/custom/cert.pem";
|
|
149
|
+
const customCertContent = "custom cert content";
|
|
150
|
+
|
|
151
|
+
// Directly set _fixedCa to avoid initializing it with real data
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
certsCache._fixedCa = fixedCa;
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
certsCache._initialized = true;
|
|
158
|
+
mockReadFileSync.mockReturnValue(customCertContent);
|
|
159
|
+
|
|
160
|
+
const ca = await certsCache.getCa(customCertPath);
|
|
161
|
+
expect(ca).toEqual([...fixedCa, customCertContent]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("CertsCache.clear should clear custom certs and reset initialized flag", () => {
|
|
165
|
+
const certsCache = CertsCache.getInstance();
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
167
|
+
// @ts-ignore
|
|
168
|
+
certsCache._customCerts.set("key", "value");
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
170
|
+
// @ts-ignore
|
|
171
|
+
certsCache._initialized = true;
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
certsCache._fixedCa = ["test"];
|
|
175
|
+
|
|
176
|
+
certsCache.clear();
|
|
177
|
+
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
expect(certsCache._customCerts.size).toBe(0);
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
expect(certsCache._initialized).toBe(false);
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
185
|
+
// @ts-ignore
|
|
186
|
+
expect(certsCache._fixedCa).toEqual([]);
|
|
187
|
+
});
|
package/src/certs.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { globalAgent } from "https";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import tls from "node:tls";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts content from either a file path or data URI
|
|
7
|
+
*/
|
|
8
|
+
export function getCertificateContent(input: string): string {
|
|
9
|
+
if (input.startsWith("data:")) {
|
|
10
|
+
// Parse data URI: data:[<mediatype>][;base64],<data>
|
|
11
|
+
const [header, data] = input.split(",");
|
|
12
|
+
if (header.includes("base64")) {
|
|
13
|
+
return Buffer.from(data, "base64").toString("utf8");
|
|
14
|
+
} else {
|
|
15
|
+
return decodeURIComponent(data);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
// Assume it's a file path
|
|
19
|
+
return fs.readFileSync(input, "utf8");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CertsCache {
|
|
24
|
+
private static instance: CertsCache;
|
|
25
|
+
private _fixedCa: string[] = [];
|
|
26
|
+
private _initialized: boolean = false;
|
|
27
|
+
private _customCerts: Map<string, string> = new Map();
|
|
28
|
+
|
|
29
|
+
private constructor() {}
|
|
30
|
+
|
|
31
|
+
public static getInstance(): CertsCache {
|
|
32
|
+
if (!CertsCache.instance) {
|
|
33
|
+
CertsCache.instance = new CertsCache();
|
|
34
|
+
}
|
|
35
|
+
return CertsCache.instance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get fixedCa(): string[] {
|
|
39
|
+
if (this._initialized) {
|
|
40
|
+
return this._fixedCa;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const globalCerts: string[] = [];
|
|
44
|
+
if (Boolean(process.env.IS_BINARY)) {
|
|
45
|
+
if (Array.isArray(globalAgent.options.ca)) {
|
|
46
|
+
globalCerts.push(
|
|
47
|
+
...globalAgent.options.ca.map((cert) => cert.toString()),
|
|
48
|
+
);
|
|
49
|
+
} else if (typeof globalAgent.options.ca !== "undefined") {
|
|
50
|
+
globalCerts.push(globalAgent.options.ca.toString());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const extraCerts: string[] = [];
|
|
55
|
+
if (process.env.NODE_EXTRA_CA_CERTS) {
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(
|
|
58
|
+
process.env.NODE_EXTRA_CA_CERTS,
|
|
59
|
+
"utf8",
|
|
60
|
+
);
|
|
61
|
+
extraCerts.push(content);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (process.env.VERBOSE_FETCH) {
|
|
64
|
+
console.error(
|
|
65
|
+
`Error reading NODE_EXTRA_CA_CERTS file: ${process.env.NODE_EXTRA_CA_CERTS}`,
|
|
66
|
+
error,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._fixedCa = Array.from(
|
|
73
|
+
new Set([...tls.rootCertificates, ...globalCerts, ...extraCerts]),
|
|
74
|
+
);
|
|
75
|
+
this._initialized = true;
|
|
76
|
+
return this._fixedCa;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getCachedCustomCert(path: string): Promise<string | undefined> {
|
|
80
|
+
if (this._customCerts.has(path)) {
|
|
81
|
+
return this._customCerts.get(path);
|
|
82
|
+
}
|
|
83
|
+
const certContent = getCertificateContent(path);
|
|
84
|
+
this._customCerts.set(path, certContent);
|
|
85
|
+
return certContent;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getAllCachedCustomCerts(
|
|
89
|
+
caBundlePath: string[] | string,
|
|
90
|
+
): Promise<string[]> {
|
|
91
|
+
const paths = Array.isArray(caBundlePath) ? caBundlePath : [caBundlePath];
|
|
92
|
+
const certs: string[] = [];
|
|
93
|
+
await Promise.all(
|
|
94
|
+
paths.map(async (path) => {
|
|
95
|
+
try {
|
|
96
|
+
const certContent = await this.getCachedCustomCert(path);
|
|
97
|
+
if (certContent) {
|
|
98
|
+
certs.push(certContent);
|
|
99
|
+
} else if (process.env.VERBOSE_FETCH) {
|
|
100
|
+
console.warn(`Empty certificate found at ${path}`);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (process.env.VERBOSE_FETCH) {
|
|
104
|
+
console.error(
|
|
105
|
+
`Error loading custom certificate from ${path}:`,
|
|
106
|
+
error,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
return certs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getCa(caBundlePath: undefined | string | string[]): Promise<string[]> {
|
|
116
|
+
if (!caBundlePath) {
|
|
117
|
+
return this.fixedCa;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const customCerts = await this.getAllCachedCustomCerts(caBundlePath);
|
|
121
|
+
return [...this.fixedCa, ...customCerts];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async clear(): Promise<void> {
|
|
125
|
+
this._customCerts.clear();
|
|
126
|
+
this._initialized = false;
|
|
127
|
+
this._fixedCa = [];
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/fetch.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { RequestOptions } from "@continuedev/config-types";
|
|
|
2
2
|
import * as followRedirects from "follow-redirects";
|
|
3
3
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
4
4
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
5
|
-
import
|
|
5
|
+
import { BodyInit, RequestInit, Response } from "node-fetch";
|
|
6
6
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
7
|
-
import
|
|
7
|
+
import fetch from "./node-fetch-patch.js";
|
|
8
|
+
import { getProxy, shouldBypassProxy } from "./util.js";
|
|
8
9
|
|
|
9
10
|
const { http, https } = (followRedirects as any).default;
|
|
10
11
|
|
|
@@ -88,18 +89,13 @@ export async function fetchwithRequestOptions(
|
|
|
88
89
|
url.host = "127.0.0.1";
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
const agentOptions = getAgentOptions(requestOptions);
|
|
92
|
+
const agentOptions = await getAgentOptions(requestOptions);
|
|
92
93
|
|
|
93
94
|
// Get proxy from options or environment variables
|
|
94
|
-
|
|
95
|
-
if (!proxy) {
|
|
96
|
-
proxy = getProxyFromEnv(url.protocol);
|
|
97
|
-
}
|
|
95
|
+
const proxy = getProxy(url.protocol, requestOptions);
|
|
98
96
|
|
|
99
97
|
// Check if should bypass proxy based on requestOptions or NO_PROXY env var
|
|
100
|
-
const shouldBypass =
|
|
101
|
-
requestOptions?.noProxy?.includes(url.hostname) ||
|
|
102
|
-
shouldBypassProxy(url.hostname);
|
|
98
|
+
const shouldBypass = shouldBypassProxy(url.hostname, requestOptions);
|
|
103
99
|
|
|
104
100
|
// Create agent
|
|
105
101
|
const protocol = url.protocol === "https:" ? https : http;
|
|
@@ -2,6 +2,8 @@ import { globalAgent } from "https";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, expect, test } from "vitest";
|
|
6
|
+
import { CertsCache } from "./certs.js";
|
|
5
7
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
6
8
|
|
|
7
9
|
// Store original env
|
|
@@ -25,6 +27,8 @@ afterEach(() => {
|
|
|
25
27
|
process.env = originalEnv;
|
|
26
28
|
globalAgent.options = originalGlobalAgentOptions;
|
|
27
29
|
|
|
30
|
+
CertsCache.getInstance().clear();
|
|
31
|
+
|
|
28
32
|
// Clean up temporary directory
|
|
29
33
|
try {
|
|
30
34
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
@@ -40,8 +44,8 @@ function createTestCertFile(filename: string, content: string): string {
|
|
|
40
44
|
return filePath;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
test("getAgentOptions returns basic configuration with default values", () => {
|
|
44
|
-
const options = getAgentOptions();
|
|
47
|
+
test("getAgentOptions returns basic configuration with default values", async () => {
|
|
48
|
+
const options = await getAgentOptions();
|
|
45
49
|
|
|
46
50
|
// Check default timeout (7200 seconds = 2 hours = 7,200,000 ms)
|
|
47
51
|
expect(options.timeout).toBe(7200000);
|
|
@@ -60,9 +64,9 @@ test("getAgentOptions returns basic configuration with default values", () => {
|
|
|
60
64
|
);
|
|
61
65
|
});
|
|
62
66
|
|
|
63
|
-
test("getAgentOptions respects custom timeout", () => {
|
|
67
|
+
test("getAgentOptions respects custom timeout", async () => {
|
|
64
68
|
const customTimeout = 300; // 5 minutes
|
|
65
|
-
const options = getAgentOptions({ timeout: customTimeout });
|
|
69
|
+
const options = await getAgentOptions({ timeout: customTimeout });
|
|
66
70
|
|
|
67
71
|
// Check timeout values (300 seconds = 300,000 ms)
|
|
68
72
|
expect(options.timeout).toBe(300000);
|
|
@@ -70,24 +74,24 @@ test("getAgentOptions respects custom timeout", () => {
|
|
|
70
74
|
expect(options.keepAliveMsecs).toBe(300000);
|
|
71
75
|
});
|
|
72
76
|
|
|
73
|
-
test("getAgentOptions uses verifySsl setting", () => {
|
|
77
|
+
test("getAgentOptions uses verifySsl setting", async () => {
|
|
74
78
|
// With verifySsl true
|
|
75
|
-
let options = getAgentOptions({ verifySsl: true });
|
|
79
|
+
let options = await getAgentOptions({ verifySsl: true });
|
|
76
80
|
expect(options.rejectUnauthorized).toBe(true);
|
|
77
81
|
|
|
78
82
|
// With verifySsl false
|
|
79
|
-
options = getAgentOptions({ verifySsl: false });
|
|
83
|
+
options = await getAgentOptions({ verifySsl: false });
|
|
80
84
|
expect(options.rejectUnauthorized).toBe(false);
|
|
81
85
|
});
|
|
82
86
|
|
|
83
|
-
test("getAgentOptions incorporates custom CA bundle paths", () => {
|
|
87
|
+
test("getAgentOptions incorporates custom CA bundle paths", async () => {
|
|
84
88
|
// Create a test CA bundle file
|
|
85
89
|
const caBundleContent =
|
|
86
90
|
"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIJAMcuSp7chAYdMA==\n-----END CERTIFICATE-----";
|
|
87
91
|
const caBundlePath = createTestCertFile("ca-bundle.pem", caBundleContent);
|
|
88
92
|
|
|
89
93
|
// Single string path
|
|
90
|
-
let options = getAgentOptions({ caBundlePath });
|
|
94
|
+
let options = await getAgentOptions({ caBundlePath });
|
|
91
95
|
|
|
92
96
|
// Verify that our test certificate is included in the CA list
|
|
93
97
|
expect(options.ca).toContain(caBundleContent);
|
|
@@ -101,7 +105,7 @@ test("getAgentOptions incorporates custom CA bundle paths", () => {
|
|
|
101
105
|
const caPath2 = createTestCertFile("ca2.pem", caContent2);
|
|
102
106
|
|
|
103
107
|
// Array of paths
|
|
104
|
-
options = getAgentOptions({
|
|
108
|
+
options = await getAgentOptions({
|
|
105
109
|
caBundlePath: [caPath1, caPath2],
|
|
106
110
|
});
|
|
107
111
|
|
|
@@ -110,21 +114,21 @@ test("getAgentOptions incorporates custom CA bundle paths", () => {
|
|
|
110
114
|
expect(options.ca).toContain(caContent2);
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
test("getAgentOptions includes global certs when running as binary", () => {
|
|
117
|
+
test("getAgentOptions includes global certs when running as binary", async () => {
|
|
114
118
|
// Set up test certs in globalAgent
|
|
115
119
|
globalAgent.options.ca = ["global-cert-1", "global-cert-2"];
|
|
116
120
|
|
|
117
121
|
// Set IS_BINARY environment variable
|
|
118
122
|
process.env.IS_BINARY = "true";
|
|
119
123
|
|
|
120
|
-
const options = getAgentOptions();
|
|
124
|
+
const options = await getAgentOptions();
|
|
121
125
|
|
|
122
126
|
// Test for global certs
|
|
123
127
|
expect(options.ca).toContain("global-cert-1");
|
|
124
128
|
expect(options.ca).toContain("global-cert-2");
|
|
125
129
|
});
|
|
126
130
|
|
|
127
|
-
test("getAgentOptions handles client certificate configuration", () => {
|
|
131
|
+
test("getAgentOptions handles client certificate configuration", async () => {
|
|
128
132
|
// Create test certificate files
|
|
129
133
|
const clientCertContent =
|
|
130
134
|
"-----BEGIN CERTIFICATE-----\nCLIENTCERT\n-----END CERTIFICATE-----";
|
|
@@ -141,14 +145,14 @@ test("getAgentOptions handles client certificate configuration", () => {
|
|
|
141
145
|
},
|
|
142
146
|
};
|
|
143
147
|
|
|
144
|
-
const options = getAgentOptions(clientCertOptions);
|
|
148
|
+
const options = await getAgentOptions(clientCertOptions);
|
|
145
149
|
|
|
146
150
|
expect(options.cert).toBe(clientCertContent);
|
|
147
151
|
expect(options.key).toBe(clientKeyContent);
|
|
148
152
|
expect(options.passphrase).toBe("secret-passphrase");
|
|
149
153
|
});
|
|
150
154
|
|
|
151
|
-
test("getAgentOptions handles client certificate without passphrase", () => {
|
|
155
|
+
test("getAgentOptions handles client certificate without passphrase", async () => {
|
|
152
156
|
// Create test certificate files
|
|
153
157
|
const clientCertContent =
|
|
154
158
|
"-----BEGIN CERTIFICATE-----\nCLIENTCERT2\n-----END CERTIFICATE-----";
|
|
@@ -164,9 +168,22 @@ test("getAgentOptions handles client certificate without passphrase", () => {
|
|
|
164
168
|
},
|
|
165
169
|
};
|
|
166
170
|
|
|
167
|
-
const options = getAgentOptions(clientCertOptions);
|
|
171
|
+
const options = await getAgentOptions(clientCertOptions);
|
|
168
172
|
|
|
169
173
|
expect(options.cert).toBe(clientCertContent);
|
|
170
174
|
expect(options.key).toBe(clientKeyContent);
|
|
171
175
|
expect(options.passphrase).toBeUndefined();
|
|
172
176
|
});
|
|
177
|
+
|
|
178
|
+
test("getAgentOptions reads NODE_EXTRA_CA_CERTS", async () => {
|
|
179
|
+
const extraCertContent =
|
|
180
|
+
"-----BEGIN CERTIFICATE-----\nEXTRA_CERT\n-----END CERTIFICATE-----";
|
|
181
|
+
const certPath = createTestCertFile("extra-cert.cert", extraCertContent);
|
|
182
|
+
|
|
183
|
+
expect(CertsCache.getInstance().fixedCa).not.toContain(extraCertContent);
|
|
184
|
+
|
|
185
|
+
CertsCache.getInstance().clear();
|
|
186
|
+
|
|
187
|
+
process.env.NODE_EXTRA_CA_CERTS = certPath;
|
|
188
|
+
expect(CertsCache.getInstance().fixedCa).toContain(extraCertContent);
|
|
189
|
+
});
|