@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/util.test.js CHANGED
@@ -1,8 +1,8 @@
1
- import { jest } from "@jest/globals";
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
- jest.resetModules();
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.12",
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": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
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
- "@types/jest": "^29.5.14",
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 fetch, { BodyInit, RequestInit, Response } from "node-fetch";
5
+ import { BodyInit, RequestInit, Response } from "node-fetch";
6
6
  import { getAgentOptions } from "./getAgentOptions.js";
7
- import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
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
- let proxy = requestOptions?.proxy;
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
+ });