@continuedev/fetch 1.0.6 → 1.0.9

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,172 @@
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
+
7
+ // Store original env
8
+ const originalEnv = process.env;
9
+ const originalGlobalAgentOptions = { ...globalAgent.options };
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(), "fetch-test-"));
17
+
18
+ process.env = { ...originalEnv };
19
+
20
+ // Reset globalAgent for each test
21
+ globalAgent.options = { ...originalGlobalAgentOptions };
22
+ });
23
+
24
+ afterEach(() => {
25
+ process.env = originalEnv;
26
+ globalAgent.options = originalGlobalAgentOptions;
27
+
28
+ // Clean up temporary directory
29
+ try {
30
+ fs.rmSync(tempDir, { recursive: true, force: true });
31
+ } catch (error) {
32
+ console.error(`Failed to remove temp directory: ${error}`);
33
+ }
34
+ });
35
+
36
+ // Helper function to create test certificate files
37
+ function createTestCertFile(filename: string, content: string): string {
38
+ const filePath = path.join(tempDir, filename);
39
+ fs.writeFileSync(filePath, content, "utf8");
40
+ return filePath;
41
+ }
42
+
43
+ test("getAgentOptions returns basic configuration with default values", () => {
44
+ const options = getAgentOptions();
45
+
46
+ // Check default timeout (7200 seconds = 2 hours = 7,200,000 ms)
47
+ expect(options.timeout).toBe(7200000);
48
+ expect(options.sessionTimeout).toBe(7200000);
49
+ expect(options.keepAliveMsecs).toBe(7200000);
50
+ expect(options.keepAlive).toBe(true);
51
+
52
+ // Verify certificates array exists and contains items
53
+ expect(options.ca).toBeInstanceOf(Array);
54
+ expect(options.ca.length).toBeGreaterThan(0);
55
+
56
+ // Verify at least one of the real TLS root certificates is included
57
+ // This assumes there's at least one certificate with "CERTIFICATE" in it
58
+ expect(options.ca.some((cert: any) => cert.includes("CERTIFICATE"))).toBe(
59
+ true,
60
+ );
61
+ });
62
+
63
+ test("getAgentOptions respects custom timeout", () => {
64
+ const customTimeout = 300; // 5 minutes
65
+ const options = getAgentOptions({ timeout: customTimeout });
66
+
67
+ // Check timeout values (300 seconds = 300,000 ms)
68
+ expect(options.timeout).toBe(300000);
69
+ expect(options.sessionTimeout).toBe(300000);
70
+ expect(options.keepAliveMsecs).toBe(300000);
71
+ });
72
+
73
+ test("getAgentOptions uses verifySsl setting", () => {
74
+ // With verifySsl true
75
+ let options = getAgentOptions({ verifySsl: true });
76
+ expect(options.rejectUnauthorized).toBe(true);
77
+
78
+ // With verifySsl false
79
+ options = getAgentOptions({ verifySsl: false });
80
+ expect(options.rejectUnauthorized).toBe(false);
81
+ });
82
+
83
+ test("getAgentOptions incorporates custom CA bundle paths", () => {
84
+ // Create a test CA bundle file
85
+ const caBundleContent =
86
+ "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIJAMcuSp7chAYdMA==\n-----END CERTIFICATE-----";
87
+ const caBundlePath = createTestCertFile("ca-bundle.pem", caBundleContent);
88
+
89
+ // Single string path
90
+ let options = getAgentOptions({ caBundlePath });
91
+
92
+ // Verify that our test certificate is included in the CA list
93
+ expect(options.ca).toContain(caBundleContent);
94
+
95
+ // Create multiple test CA bundle files
96
+ const caContent1 =
97
+ "-----BEGIN CERTIFICATE-----\nABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n-----END CERTIFICATE-----";
98
+ const caContent2 =
99
+ "-----BEGIN CERTIFICATE-----\n0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\n-----END CERTIFICATE-----";
100
+ const caPath1 = createTestCertFile("ca1.pem", caContent1);
101
+ const caPath2 = createTestCertFile("ca2.pem", caContent2);
102
+
103
+ // Array of paths
104
+ options = getAgentOptions({
105
+ caBundlePath: [caPath1, caPath2],
106
+ });
107
+
108
+ // Verify that both test certificates are included in the CA list
109
+ expect(options.ca).toContain(caContent1);
110
+ expect(options.ca).toContain(caContent2);
111
+ });
112
+
113
+ test("getAgentOptions includes global certs when running as binary", () => {
114
+ // Set up test certs in globalAgent
115
+ globalAgent.options.ca = ["global-cert-1", "global-cert-2"];
116
+
117
+ // Set IS_BINARY environment variable
118
+ process.env.IS_BINARY = "true";
119
+
120
+ const options = getAgentOptions();
121
+
122
+ // Test for global certs
123
+ expect(options.ca).toContain("global-cert-1");
124
+ expect(options.ca).toContain("global-cert-2");
125
+ });
126
+
127
+ test("getAgentOptions handles client certificate configuration", () => {
128
+ // Create test certificate files
129
+ const clientCertContent =
130
+ "-----BEGIN CERTIFICATE-----\nCLIENTCERT\n-----END CERTIFICATE-----";
131
+ const clientKeyContent =
132
+ "-----BEGIN PRIVATE KEY-----\nCLIENTKEY\n-----END PRIVATE KEY-----";
133
+ const certPath = createTestCertFile("client.cert", clientCertContent);
134
+ const keyPath = createTestCertFile("client.key", clientKeyContent);
135
+
136
+ const clientCertOptions = {
137
+ clientCertificate: {
138
+ cert: certPath,
139
+ key: keyPath,
140
+ passphrase: "secret-passphrase",
141
+ },
142
+ };
143
+
144
+ const options = getAgentOptions(clientCertOptions);
145
+
146
+ expect(options.cert).toBe(clientCertContent);
147
+ expect(options.key).toBe(clientKeyContent);
148
+ expect(options.passphrase).toBe("secret-passphrase");
149
+ });
150
+
151
+ test("getAgentOptions handles client certificate without passphrase", () => {
152
+ // Create test certificate files
153
+ const clientCertContent =
154
+ "-----BEGIN CERTIFICATE-----\nCLIENTCERT2\n-----END CERTIFICATE-----";
155
+ const clientKeyContent =
156
+ "-----BEGIN PRIVATE KEY-----\nCLIENTKEY2\n-----END PRIVATE KEY-----";
157
+ const certPath = createTestCertFile("client2.cert", clientCertContent);
158
+ const keyPath = createTestCertFile("client2.key", clientKeyContent);
159
+
160
+ const clientCertOptions = {
161
+ clientCertificate: {
162
+ cert: certPath,
163
+ key: keyPath,
164
+ },
165
+ };
166
+
167
+ const options = getAgentOptions(clientCertOptions);
168
+
169
+ expect(options.cert).toBe(clientCertContent);
170
+ expect(options.key).toBe(clientKeyContent);
171
+ expect(options.passphrase).toBeUndefined();
172
+ });
package/src/fetch.ts CHANGED
@@ -1,12 +1,10 @@
1
- import { globalAgent } from "https";
2
- import * as fs from "node:fs";
3
- import tls from "node:tls";
4
-
5
1
  import { RequestOptions } from "@continuedev/config-types";
6
2
  import * as followRedirects from "follow-redirects";
7
3
  import { HttpProxyAgent } from "http-proxy-agent";
8
4
  import { HttpsProxyAgent } from "https-proxy-agent";
9
5
  import fetch, { RequestInit, Response } from "node-fetch";
6
+ import { getAgentOptions } from "./getAgentOptions.js";
7
+ import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
10
8
 
11
9
  const { http, https } = (followRedirects as any).default;
12
10
 
@@ -20,59 +18,23 @@ export async function fetchwithRequestOptions(
20
18
  url.host = "127.0.0.1";
21
19
  }
22
20
 
23
- const TIMEOUT = 7200; // 7200 seconds = 2 hours
21
+ const agentOptions = getAgentOptions(requestOptions);
24
22
 
25
- let globalCerts: string[] = [];
26
- if (process.env.IS_BINARY) {
27
- if (Array.isArray(globalAgent.options.ca)) {
28
- globalCerts = [...globalAgent.options.ca.map((cert) => cert.toString())];
29
- } else if (typeof globalAgent.options.ca !== "undefined") {
30
- globalCerts.push(globalAgent.options.ca.toString());
31
- }
32
- }
33
- const ca = Array.from(new Set([...tls.rootCertificates, ...globalCerts]));
34
- const customCerts =
35
- typeof requestOptions?.caBundlePath === "string"
36
- ? [requestOptions?.caBundlePath]
37
- : requestOptions?.caBundlePath;
38
- if (customCerts) {
39
- ca.push(
40
- ...customCerts.map((customCert) => fs.readFileSync(customCert, "utf8")),
41
- );
23
+ // Get proxy from options or environment variables
24
+ let proxy = requestOptions?.proxy;
25
+ if (!proxy) {
26
+ proxy = getProxyFromEnv(url.protocol);
42
27
  }
43
28
 
44
- const timeout = (requestOptions?.timeout ?? TIMEOUT) * 1000; // measured in ms
45
-
46
- const agentOptions: { [key: string]: any } = {
47
- ca,
48
- rejectUnauthorized: requestOptions?.verifySsl,
49
- timeout,
50
- sessionTimeout: timeout,
51
- keepAlive: true,
52
- keepAliveMsecs: timeout,
53
- };
54
-
55
- // Handle ClientCertificateOptions
56
- if (requestOptions?.clientCertificate) {
57
- agentOptions.cert = fs.readFileSync(
58
- requestOptions.clientCertificate.cert,
59
- "utf8",
60
- );
61
- agentOptions.key = fs.readFileSync(
62
- requestOptions.clientCertificate.key,
63
- "utf8",
64
- );
65
- if (requestOptions.clientCertificate.passphrase) {
66
- agentOptions.passphrase = requestOptions.clientCertificate.passphrase;
67
- }
68
- }
69
-
70
- const proxy = requestOptions?.proxy;
29
+ // Check if should bypass proxy based on requestOptions or NO_PROXY env var
30
+ const shouldBypass =
31
+ requestOptions?.noProxy?.includes(url.hostname) ||
32
+ shouldBypassProxy(url.hostname);
71
33
 
72
34
  // Create agent
73
35
  const protocol = url.protocol === "https:" ? https : http;
74
36
  const agent =
75
- proxy && !requestOptions?.noProxy?.includes(url.hostname)
37
+ proxy && !shouldBypass
76
38
  ? protocol === https
77
39
  ? new HttpsProxyAgent(proxy, agentOptions)
78
40
  : new HttpProxyAgent(proxy, agentOptions)
@@ -107,19 +69,30 @@ export async function fetchwithRequestOptions(
107
69
  }
108
70
 
109
71
  // fetch the request with the provided options
110
- const resp = await fetch(url, {
111
- ...init,
112
- body: updatedBody ?? init?.body,
113
- headers: headers,
114
- agent: agent,
115
- });
72
+ try {
73
+ const resp = await fetch(url, {
74
+ ...init,
75
+ body: updatedBody ?? init?.body,
76
+ headers: headers,
77
+ agent: agent,
78
+ });
79
+
80
+ if (!resp.ok) {
81
+ const requestId = resp.headers.get("x-request-id");
82
+ if (requestId) {
83
+ console.log(`Request ID: ${requestId}, Status: ${resp.status}`);
84
+ }
85
+ }
116
86
 
117
- if (!resp.ok) {
118
- const requestId = resp.headers.get("x-request-id");
119
- if (requestId) {
120
- console.log(`Request ID: ${requestId}, Status: ${resp.status}`);
87
+ return resp;
88
+ } catch (error) {
89
+ if (error instanceof Error && error.name === "AbortError") {
90
+ // Return a Response object that streamResponse etc can handle
91
+ return new Response(null, {
92
+ status: 499, // Client Closed Request
93
+ statusText: "Client Closed Request",
94
+ });
121
95
  }
96
+ throw error;
122
97
  }
123
-
124
- return resp;
125
98
  }
@@ -0,0 +1,62 @@
1
+ import { globalAgent } from "https";
2
+ import * as fs from "node:fs";
3
+ import tls from "node:tls";
4
+
5
+ import { RequestOptions } from "@continuedev/config-types";
6
+
7
+ /**
8
+ * Prepares agent options based on request options and certificates
9
+ */
10
+ export function getAgentOptions(requestOptions?: RequestOptions): {
11
+ [key: string]: any;
12
+ } {
13
+ const TIMEOUT = 7200; // 7200 seconds = 2 hours
14
+ const timeout = (requestOptions?.timeout ?? TIMEOUT) * 1000; // measured in ms
15
+
16
+ // Get root certificates
17
+ let globalCerts: string[] = [];
18
+ if (process.env.IS_BINARY) {
19
+ if (Array.isArray(globalAgent.options.ca)) {
20
+ globalCerts = [...globalAgent.options.ca.map((cert) => cert.toString())];
21
+ } else if (typeof globalAgent.options.ca !== "undefined") {
22
+ globalCerts.push(globalAgent.options.ca.toString());
23
+ }
24
+ }
25
+
26
+ const ca = Array.from(new Set([...tls.rootCertificates, ...globalCerts]));
27
+ const customCerts =
28
+ typeof requestOptions?.caBundlePath === "string"
29
+ ? [requestOptions?.caBundlePath]
30
+ : requestOptions?.caBundlePath;
31
+ if (customCerts) {
32
+ ca.push(
33
+ ...customCerts.map((customCert) => fs.readFileSync(customCert, "utf8")),
34
+ );
35
+ }
36
+
37
+ const agentOptions: { [key: string]: any } = {
38
+ ca,
39
+ rejectUnauthorized: requestOptions?.verifySsl,
40
+ timeout,
41
+ sessionTimeout: timeout,
42
+ keepAlive: true,
43
+ keepAliveMsecs: timeout,
44
+ };
45
+
46
+ // Handle ClientCertificateOptions
47
+ if (requestOptions?.clientCertificate) {
48
+ agentOptions.cert = fs.readFileSync(
49
+ requestOptions.clientCertificate.cert,
50
+ "utf8",
51
+ );
52
+ agentOptions.key = fs.readFileSync(
53
+ requestOptions.clientCertificate.key,
54
+ "utf8",
55
+ );
56
+ if (requestOptions.clientCertificate.passphrase) {
57
+ agentOptions.passphrase = requestOptions.clientCertificate.passphrase;
58
+ }
59
+ }
60
+
61
+ return agentOptions;
62
+ }
@@ -0,0 +1,49 @@
1
+ // The parseDataLine function is not exported, so we need to import it
2
+ // indirectly by re-exporting it for testing purposes
3
+ import { parseDataLine } from "./stream.js";
4
+
5
+ describe("parseDataLine", () => {
6
+ test("parseDataLine should parse valid JSON data with 'data: ' prefix", () => {
7
+ const line = 'data: {"message":"hello","status":"ok"}';
8
+ const result = parseDataLine(line);
9
+ expect(result).toEqual({ message: "hello", status: "ok" });
10
+ });
11
+
12
+ test("parseDataLine should parse valid JSON data with 'data:' prefix (no space)", () => {
13
+ const line = 'data:{"message":"hello","status":"ok"}';
14
+ const result = parseDataLine(line);
15
+ expect(result).toEqual({ message: "hello", status: "ok" });
16
+ });
17
+
18
+ test("parseDataLine should throw error for malformed JSON", () => {
19
+ const line = "data: {invalid json}";
20
+ expect(() => parseDataLine(line)).toThrow(
21
+ "Malformed JSON sent from server",
22
+ );
23
+ });
24
+
25
+ test("parseDataLine should throw error when data contains error field", () => {
26
+ const line = 'data: {"error":"something went wrong"}';
27
+ expect(() => parseDataLine(line)).toThrow(
28
+ "Error streaming response: something went wrong",
29
+ );
30
+ });
31
+
32
+ test("parseDataLine should handle empty objects", () => {
33
+ const line = "data: {}";
34
+ const result = parseDataLine(line);
35
+ expect(result).toEqual({});
36
+ });
37
+
38
+ test("parseDataLine should handle arrays", () => {
39
+ const line = "data: [1,2,3]";
40
+ const result = parseDataLine(line);
41
+ expect(result).toEqual([1, 2, 3]);
42
+ });
43
+
44
+ test("parseDataLine should handle nested objects", () => {
45
+ const line = 'data: {"user":{"name":"John","age":30}}';
46
+ const result = parseDataLine(line);
47
+ expect(result).toEqual({ user: { name: "John", age: 30 } });
48
+ });
49
+ });
package/src/stream.ts CHANGED
@@ -10,6 +10,9 @@ export async function* toAsyncIterable(
10
10
  export async function* streamResponse(
11
11
  response: Response,
12
12
  ): AsyncGenerator<string> {
13
+ if (response.status === 499) {
14
+ return; // In case of client-side cancellation, just return
15
+ }
13
16
  if (response.status !== 200) {
14
17
  throw new Error(await response.text());
15
18
  }
@@ -21,26 +24,34 @@ export async function* streamResponse(
21
24
  // Get the major version of Node.js
22
25
  const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10);
23
26
 
24
- if (nodeMajorVersion >= 20) {
25
- // Use the new API for Node 20 and above
26
- const stream = (ReadableStream as any).from(response.body);
27
- for await (const chunk of stream.pipeThrough(
28
- new TextDecoderStream("utf-8"),
29
- )) {
30
- yield chunk;
27
+ try {
28
+ if (nodeMajorVersion >= 20) {
29
+ // Use the new API for Node 20 and above
30
+ const stream = (ReadableStream as any).from(response.body);
31
+ for await (const chunk of stream.pipeThrough(
32
+ new TextDecoderStream("utf-8"),
33
+ )) {
34
+ yield chunk;
35
+ }
36
+ } else {
37
+ // Fallback for Node versions below 20
38
+ // Streaming with this method doesn't work as version 20+ does
39
+ const decoder = new TextDecoder("utf-8");
40
+ const nodeStream = response.body as unknown as NodeJS.ReadableStream;
41
+ for await (const chunk of toAsyncIterable(nodeStream)) {
42
+ yield decoder.decode(chunk, { stream: true });
43
+ }
31
44
  }
32
- } else {
33
- // Fallback for Node versions below 20
34
- // Streaming with this method doesn't work as version 20+ does
35
- const decoder = new TextDecoder("utf-8");
36
- const nodeStream = response.body as unknown as NodeJS.ReadableStream;
37
- for await (const chunk of toAsyncIterable(nodeStream)) {
38
- yield decoder.decode(chunk, { stream: true });
45
+ } catch (e) {
46
+ if (e instanceof Error && e.name.startsWith("AbortError")) {
47
+ return; // In case of client-side cancellation, just return
39
48
  }
49
+ throw e;
40
50
  }
41
51
  }
42
52
 
43
- function parseDataLine(line: string): any {
53
+ // Export for testing purposes
54
+ export function parseDataLine(line: string): any {
44
55
  const json = line.startsWith("data: ")
45
56
  ? line.slice("data: ".length)
46
57
  : line.slice("data:".length);
@@ -53,6 +64,14 @@ function parseDataLine(line: string): any {
53
64
 
54
65
  return data;
55
66
  } catch (e) {
67
+ // If the error was thrown by our error check, rethrow it
68
+ if (
69
+ e instanceof Error &&
70
+ e.message.startsWith("Error streaming response:")
71
+ ) {
72
+ throw e;
73
+ }
74
+ // Otherwise it's a JSON parsing error
56
75
  throw new Error(`Malformed JSON sent from server: ${json}`);
57
76
  }
58
77
  }
@@ -0,0 +1,102 @@
1
+ import { jest } from "@jest/globals";
2
+ import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
3
+
4
+ // Reset environment variables after each test
5
+ afterEach(() => {
6
+ jest.resetModules();
7
+ process.env = {};
8
+ });
9
+
10
+ // Tests for getProxyFromEnv
11
+ test("getProxyFromEnv returns undefined when no proxy is set", () => {
12
+ expect(getProxyFromEnv("http:")).toBeUndefined();
13
+ expect(getProxyFromEnv("https:")).toBeUndefined();
14
+ });
15
+
16
+ test("getProxyFromEnv returns HTTP_PROXY for http protocol", () => {
17
+ process.env.HTTP_PROXY = "http://proxy.example.com";
18
+ expect(getProxyFromEnv("http:")).toBe("http://proxy.example.com");
19
+ });
20
+
21
+ test("getProxyFromEnv returns lowercase http_proxy for http protocol", () => {
22
+ process.env.http_proxy = "http://proxy.example.com";
23
+ expect(getProxyFromEnv("http:")).toBe("http://proxy.example.com");
24
+ });
25
+
26
+ test("getProxyFromEnv prefers HTTP_PROXY over http_proxy for http protocol", () => {
27
+ process.env.HTTP_PROXY = "http://upper.example.com";
28
+ process.env.http_proxy = "http://lower.example.com";
29
+ expect(getProxyFromEnv("http:")).toBe("http://upper.example.com");
30
+ });
31
+
32
+ test("getProxyFromEnv returns HTTPS_PROXY for https protocol", () => {
33
+ process.env.HTTPS_PROXY = "https://secure.example.com";
34
+ expect(getProxyFromEnv("https:")).toBe("https://secure.example.com");
35
+ });
36
+
37
+ test("getProxyFromEnv returns lowercase https_proxy for https protocol", () => {
38
+ process.env.https_proxy = "https://secure.example.com";
39
+ expect(getProxyFromEnv("https:")).toBe("https://secure.example.com");
40
+ });
41
+
42
+ test("getProxyFromEnv falls back to HTTP_PROXY for https protocol when HTTPS_PROXY is not set", () => {
43
+ process.env.HTTP_PROXY = "http://fallback.example.com";
44
+ expect(getProxyFromEnv("https:")).toBe("http://fallback.example.com");
45
+ });
46
+
47
+ test("getProxyFromEnv prefers HTTPS_PROXY over other env vars for https protocol", () => {
48
+ process.env.HTTPS_PROXY = "https://preferred.example.com";
49
+ process.env.https_proxy = "https://notused1.example.com";
50
+ process.env.HTTP_PROXY = "http://notused2.example.com";
51
+ process.env.http_proxy = "http://notused3.example.com";
52
+ expect(getProxyFromEnv("https:")).toBe("https://preferred.example.com");
53
+ });
54
+
55
+ // Tests for shouldBypassProxy
56
+ test("shouldBypassProxy returns false when NO_PROXY is not set", () => {
57
+ expect(shouldBypassProxy("example.com")).toBe(false);
58
+ });
59
+
60
+ test("shouldBypassProxy returns true for exact hostname match", () => {
61
+ process.env.NO_PROXY = "example.com,another.com";
62
+ expect(shouldBypassProxy("example.com")).toBe(true);
63
+ });
64
+
65
+ test("shouldBypassProxy returns false when hostname doesn't match any NO_PROXY entry", () => {
66
+ process.env.NO_PROXY = "example.com,another.com";
67
+ expect(shouldBypassProxy("different.com")).toBe(false);
68
+ });
69
+
70
+ test("shouldBypassProxy handles lowercase no_proxy", () => {
71
+ process.env.no_proxy = "example.com";
72
+ expect(shouldBypassProxy("example.com")).toBe(true);
73
+ });
74
+
75
+ test("shouldBypassProxy works with wildcard domains", () => {
76
+ process.env.NO_PROXY = "*.example.com";
77
+ expect(shouldBypassProxy("sub.example.com")).toBe(true);
78
+ expect(shouldBypassProxy("example.com")).toBe(false);
79
+ expect(shouldBypassProxy("different.com")).toBe(false);
80
+ });
81
+
82
+ test("shouldBypassProxy works with domain suffix", () => {
83
+ process.env.NO_PROXY = ".example.com";
84
+ expect(shouldBypassProxy("sub.example.com")).toBe(true);
85
+ expect(shouldBypassProxy("example.com")).toBe(true);
86
+ expect(shouldBypassProxy("different.com")).toBe(false);
87
+ });
88
+
89
+ test("shouldBypassProxy handles multiple entries with different patterns", () => {
90
+ process.env.NO_PROXY = "internal.local,*.example.com,.test.com";
91
+ expect(shouldBypassProxy("internal.local")).toBe(true);
92
+ expect(shouldBypassProxy("sub.example.com")).toBe(true);
93
+ expect(shouldBypassProxy("sub.test.com")).toBe(true);
94
+ expect(shouldBypassProxy("test.com")).toBe(true);
95
+ expect(shouldBypassProxy("example.org")).toBe(false);
96
+ });
97
+
98
+ test("shouldBypassProxy ignores whitespace in NO_PROXY", () => {
99
+ process.env.NO_PROXY = " example.com , *.test.org ";
100
+ expect(shouldBypassProxy("example.com")).toBe(true);
101
+ expect(shouldBypassProxy("subdomain.test.org")).toBe(true);
102
+ });
package/src/util.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Gets the proxy settings from environment variables
3
+ * @param protocol The URL protocol (http: or https:)
4
+ * @returns The proxy URL if available, otherwise undefined
5
+ */
6
+ export function getProxyFromEnv(protocol: string): string | undefined {
7
+ if (protocol === "https:") {
8
+ return (
9
+ process.env.HTTPS_PROXY ||
10
+ process.env.https_proxy ||
11
+ process.env.HTTP_PROXY ||
12
+ process.env.http_proxy
13
+ );
14
+ } else {
15
+ return process.env.HTTP_PROXY || process.env.http_proxy;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Checks if a hostname should bypass proxy based on NO_PROXY environment variable
21
+ * @param hostname The hostname to check
22
+ * @returns True if the hostname should bypass proxy
23
+ */
24
+ export function shouldBypassProxy(hostname: string): boolean {
25
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy;
26
+ if (!noProxy) return false;
27
+
28
+ const noProxyItems = noProxy.split(",").map((item) => item.trim());
29
+
30
+ return noProxyItems.some((item) => {
31
+ // Exact match
32
+ if (item === hostname) return true;
33
+
34
+ // Wildcard domain match (*.example.com)
35
+ if (item.startsWith("*.") && hostname.endsWith(item.substring(1)))
36
+ return true;
37
+
38
+ // Domain suffix match (.example.com)
39
+ if (item.startsWith(".") && hostname.endsWith(item.slice(1))) return true;
40
+
41
+ return false;
42
+ });
43
+ }
package/tsconfig.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "resolveJsonModule": true,
15
15
  "isolatedModules": true,
16
16
  "noEmitOnError": false,
17
- "types": ["node"],
17
+ "types": ["node", "jest"],
18
18
  "outDir": "dist",
19
19
  "declaration": true
20
20
  // "sourceMap": true
package/jest.config.d.ts DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * For a detailed explanation regarding each configuration property, visit:
3
- * https://jestjs.io/docs/configuration
4
- */
5
- import type { Config } from 'jest';
6
- declare const config: Config;
7
- export default config;