@continuedev/fetch 1.0.5 → 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.
- package/dist/fetch.js +38 -47
- package/dist/fetch.test.d.ts +1 -0
- package/dist/fetch.test.js +130 -0
- package/dist/getAgentOptions.d.ts +7 -0
- package/dist/getAgentOptions.js +44 -0
- package/dist/stream.d.ts +1 -0
- package/dist/stream.js +31 -13
- package/dist/stream.test.d.ts +1 -0
- package/dist/stream.test.js +38 -0
- package/dist/util.d.ts +12 -0
- package/dist/util.js +39 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.js +85 -0
- package/jest.config.mjs +20 -0
- package/package.json +7 -3
- package/src/fetch.test.ts +172 -0
- package/src/fetch.ts +38 -58
- package/src/getAgentOptions.ts +62 -0
- package/src/stream.test.ts +49 -0
- package/src/stream.ts +34 -15
- package/src/util.test.ts +102 -0
- package/src/util.ts +43 -0
- package/tsconfig.json +1 -1
- package/jest.config.d.ts +0 -7
- package/jest.config.js +0 -143
- package/jest.config.ts +0 -199
package/dist/fetch.js
CHANGED
|
@@ -1,54 +1,27 @@
|
|
|
1
|
-
import { globalAgent } from "https";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import tls from "node:tls";
|
|
4
1
|
import * as followRedirects from "follow-redirects";
|
|
5
2
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
6
3
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
7
|
-
import fetch from "node-fetch";
|
|
4
|
+
import fetch, { Response } from "node-fetch";
|
|
5
|
+
import { getAgentOptions } from "./getAgentOptions.js";
|
|
6
|
+
import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
|
|
8
7
|
const { http, https } = followRedirects.default;
|
|
9
|
-
export function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
8
|
+
export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
10
9
|
const url = typeof url_ === "string" ? new URL(url_) : url_;
|
|
11
10
|
if (url.host === "localhost") {
|
|
12
11
|
url.host = "127.0.0.1";
|
|
13
12
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
else if (typeof globalAgent.options.ca !== "undefined") {
|
|
21
|
-
globalCerts.push(globalAgent.options.ca.toString());
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const ca = Array.from(new Set([...tls.rootCertificates, ...globalCerts]));
|
|
25
|
-
const customCerts = typeof requestOptions?.caBundlePath === "string"
|
|
26
|
-
? [requestOptions?.caBundlePath]
|
|
27
|
-
: requestOptions?.caBundlePath;
|
|
28
|
-
if (customCerts) {
|
|
29
|
-
ca.push(...customCerts.map((customCert) => fs.readFileSync(customCert, "utf8")));
|
|
30
|
-
}
|
|
31
|
-
const timeout = (requestOptions?.timeout ?? TIMEOUT) * 1000; // measured in ms
|
|
32
|
-
const agentOptions = {
|
|
33
|
-
ca,
|
|
34
|
-
rejectUnauthorized: requestOptions?.verifySsl,
|
|
35
|
-
timeout,
|
|
36
|
-
sessionTimeout: timeout,
|
|
37
|
-
keepAlive: true,
|
|
38
|
-
keepAliveMsecs: timeout,
|
|
39
|
-
};
|
|
40
|
-
// Handle ClientCertificateOptions
|
|
41
|
-
if (requestOptions?.clientCertificate) {
|
|
42
|
-
agentOptions.cert = fs.readFileSync(requestOptions.clientCertificate.cert, "utf8");
|
|
43
|
-
agentOptions.key = fs.readFileSync(requestOptions.clientCertificate.key, "utf8");
|
|
44
|
-
if (requestOptions.clientCertificate.passphrase) {
|
|
45
|
-
agentOptions.passphrase = requestOptions.clientCertificate.passphrase;
|
|
46
|
-
}
|
|
13
|
+
const agentOptions = getAgentOptions(requestOptions);
|
|
14
|
+
// Get proxy from options or environment variables
|
|
15
|
+
let proxy = requestOptions?.proxy;
|
|
16
|
+
if (!proxy) {
|
|
17
|
+
proxy = getProxyFromEnv(url.protocol);
|
|
47
18
|
}
|
|
48
|
-
|
|
19
|
+
// Check if should bypass proxy based on requestOptions or NO_PROXY env var
|
|
20
|
+
const shouldBypass = requestOptions?.noProxy?.includes(url.hostname) ||
|
|
21
|
+
shouldBypassProxy(url.hostname);
|
|
49
22
|
// Create agent
|
|
50
23
|
const protocol = url.protocol === "https:" ? https : http;
|
|
51
|
-
const agent = proxy && !
|
|
24
|
+
const agent = proxy && !shouldBypass
|
|
52
25
|
? protocol === https
|
|
53
26
|
? new HttpsProxyAgent(proxy, agentOptions)
|
|
54
27
|
: new HttpProxyAgent(proxy, agentOptions)
|
|
@@ -80,11 +53,29 @@ export function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
|
80
53
|
console.log("Unable to parse HTTP request body: ", e);
|
|
81
54
|
}
|
|
82
55
|
// fetch the request with the provided options
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
56
|
+
try {
|
|
57
|
+
const resp = await fetch(url, {
|
|
58
|
+
...init,
|
|
59
|
+
body: updatedBody ?? init?.body,
|
|
60
|
+
headers: headers,
|
|
61
|
+
agent: agent,
|
|
62
|
+
});
|
|
63
|
+
if (!resp.ok) {
|
|
64
|
+
const requestId = resp.headers.get("x-request-id");
|
|
65
|
+
if (requestId) {
|
|
66
|
+
console.log(`Request ID: ${requestId}, Status: ${resp.status}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return resp;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
73
|
+
// Return a Response object that streamResponse etc can handle
|
|
74
|
+
return new Response(null, {
|
|
75
|
+
status: 499, // Client Closed Request
|
|
76
|
+
statusText: "Client Closed Request",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
90
81
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { globalAgent } from "https";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import tls from "node:tls";
|
|
4
|
+
/**
|
|
5
|
+
* Prepares agent options based on request options and certificates
|
|
6
|
+
*/
|
|
7
|
+
export function getAgentOptions(requestOptions) {
|
|
8
|
+
const TIMEOUT = 7200; // 7200 seconds = 2 hours
|
|
9
|
+
const timeout = (requestOptions?.timeout ?? TIMEOUT) * 1000; // measured in ms
|
|
10
|
+
// Get root certificates
|
|
11
|
+
let globalCerts = [];
|
|
12
|
+
if (process.env.IS_BINARY) {
|
|
13
|
+
if (Array.isArray(globalAgent.options.ca)) {
|
|
14
|
+
globalCerts = [...globalAgent.options.ca.map((cert) => cert.toString())];
|
|
15
|
+
}
|
|
16
|
+
else if (typeof globalAgent.options.ca !== "undefined") {
|
|
17
|
+
globalCerts.push(globalAgent.options.ca.toString());
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const ca = Array.from(new Set([...tls.rootCertificates, ...globalCerts]));
|
|
21
|
+
const customCerts = typeof requestOptions?.caBundlePath === "string"
|
|
22
|
+
? [requestOptions?.caBundlePath]
|
|
23
|
+
: requestOptions?.caBundlePath;
|
|
24
|
+
if (customCerts) {
|
|
25
|
+
ca.push(...customCerts.map((customCert) => fs.readFileSync(customCert, "utf8")));
|
|
26
|
+
}
|
|
27
|
+
const agentOptions = {
|
|
28
|
+
ca,
|
|
29
|
+
rejectUnauthorized: requestOptions?.verifySsl,
|
|
30
|
+
timeout,
|
|
31
|
+
sessionTimeout: timeout,
|
|
32
|
+
keepAlive: true,
|
|
33
|
+
keepAliveMsecs: timeout,
|
|
34
|
+
};
|
|
35
|
+
// Handle ClientCertificateOptions
|
|
36
|
+
if (requestOptions?.clientCertificate) {
|
|
37
|
+
agentOptions.cert = fs.readFileSync(requestOptions.clientCertificate.cert, "utf8");
|
|
38
|
+
agentOptions.key = fs.readFileSync(requestOptions.clientCertificate.key, "utf8");
|
|
39
|
+
if (requestOptions.clientCertificate.passphrase) {
|
|
40
|
+
agentOptions.passphrase = requestOptions.clientCertificate.passphrase;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return agentOptions;
|
|
44
|
+
}
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function toAsyncIterable(nodeReadable: NodeJS.ReadableStream): AsyncGenerator<Uint8Array>;
|
|
2
2
|
export declare function streamResponse(response: Response): AsyncGenerator<string>;
|
|
3
|
+
export declare function parseDataLine(line: string): any;
|
|
3
4
|
export declare function streamSse(response: Response): AsyncGenerator<any>;
|
|
4
5
|
export declare function streamJSON(response: Response): AsyncGenerator<any>;
|
package/dist/stream.js
CHANGED
|
@@ -5,6 +5,9 @@ export async function* toAsyncIterable(nodeReadable) {
|
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
7
|
export async function* streamResponse(response) {
|
|
8
|
+
if (response.status === 499) {
|
|
9
|
+
return; // In case of client-side cancellation, just return
|
|
10
|
+
}
|
|
8
11
|
if (response.status !== 200) {
|
|
9
12
|
throw new Error(await response.text());
|
|
10
13
|
}
|
|
@@ -13,24 +16,33 @@ export async function* streamResponse(response) {
|
|
|
13
16
|
}
|
|
14
17
|
// Get the major version of Node.js
|
|
15
18
|
const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
if (nodeMajorVersion >= 20) {
|
|
21
|
+
// Use the new API for Node 20 and above
|
|
22
|
+
const stream = ReadableStream.from(response.body);
|
|
23
|
+
for await (const chunk of stream.pipeThrough(new TextDecoderStream("utf-8"))) {
|
|
24
|
+
yield chunk;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Fallback for Node versions below 20
|
|
29
|
+
// Streaming with this method doesn't work as version 20+ does
|
|
30
|
+
const decoder = new TextDecoder("utf-8");
|
|
31
|
+
const nodeStream = response.body;
|
|
32
|
+
for await (const chunk of toAsyncIterable(nodeStream)) {
|
|
33
|
+
yield decoder.decode(chunk, { stream: true });
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const decoder = new TextDecoder("utf-8");
|
|
27
|
-
const nodeStream = response.body;
|
|
28
|
-
for await (const chunk of toAsyncIterable(nodeStream)) {
|
|
29
|
-
yield decoder.decode(chunk, { stream: true });
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof Error && e.name.startsWith("AbortError")) {
|
|
39
|
+
return; // In case of client-side cancellation, just return
|
|
30
40
|
}
|
|
41
|
+
throw e;
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
|
-
|
|
44
|
+
// Export for testing purposes
|
|
45
|
+
export function parseDataLine(line) {
|
|
34
46
|
const json = line.startsWith("data: ")
|
|
35
47
|
? line.slice("data: ".length)
|
|
36
48
|
: line.slice("data:".length);
|
|
@@ -42,6 +54,12 @@ function parseDataLine(line) {
|
|
|
42
54
|
return data;
|
|
43
55
|
}
|
|
44
56
|
catch (e) {
|
|
57
|
+
// If the error was thrown by our error check, rethrow it
|
|
58
|
+
if (e instanceof Error &&
|
|
59
|
+
e.message.startsWith("Error streaming response:")) {
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
// Otherwise it's a JSON parsing error
|
|
45
63
|
throw new Error(`Malformed JSON sent from server: ${json}`);
|
|
46
64
|
}
|
|
47
65
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
describe("parseDataLine", () => {
|
|
5
|
+
test("parseDataLine should parse valid JSON data with 'data: ' prefix", () => {
|
|
6
|
+
const line = 'data: {"message":"hello","status":"ok"}';
|
|
7
|
+
const result = parseDataLine(line);
|
|
8
|
+
expect(result).toEqual({ message: "hello", status: "ok" });
|
|
9
|
+
});
|
|
10
|
+
test("parseDataLine should parse valid JSON data with 'data:' prefix (no space)", () => {
|
|
11
|
+
const line = 'data:{"message":"hello","status":"ok"}';
|
|
12
|
+
const result = parseDataLine(line);
|
|
13
|
+
expect(result).toEqual({ message: "hello", status: "ok" });
|
|
14
|
+
});
|
|
15
|
+
test("parseDataLine should throw error for malformed JSON", () => {
|
|
16
|
+
const line = "data: {invalid json}";
|
|
17
|
+
expect(() => parseDataLine(line)).toThrow("Malformed JSON sent from server");
|
|
18
|
+
});
|
|
19
|
+
test("parseDataLine should throw error when data contains error field", () => {
|
|
20
|
+
const line = 'data: {"error":"something went wrong"}';
|
|
21
|
+
expect(() => parseDataLine(line)).toThrow("Error streaming response: something went wrong");
|
|
22
|
+
});
|
|
23
|
+
test("parseDataLine should handle empty objects", () => {
|
|
24
|
+
const line = "data: {}";
|
|
25
|
+
const result = parseDataLine(line);
|
|
26
|
+
expect(result).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
test("parseDataLine should handle arrays", () => {
|
|
29
|
+
const line = "data: [1,2,3]";
|
|
30
|
+
const result = parseDataLine(line);
|
|
31
|
+
expect(result).toEqual([1, 2, 3]);
|
|
32
|
+
});
|
|
33
|
+
test("parseDataLine should handle nested objects", () => {
|
|
34
|
+
const line = 'data: {"user":{"name":"John","age":30}}';
|
|
35
|
+
const result = parseDataLine(line);
|
|
36
|
+
expect(result).toEqual({ user: { name: "John", age: 30 } });
|
|
37
|
+
});
|
|
38
|
+
});
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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 declare function getProxyFromEnv(protocol: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a hostname should bypass proxy based on NO_PROXY environment variable
|
|
9
|
+
* @param hostname The hostname to check
|
|
10
|
+
* @returns True if the hostname should bypass proxy
|
|
11
|
+
*/
|
|
12
|
+
export declare function shouldBypassProxy(hostname: string): boolean;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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) {
|
|
7
|
+
if (protocol === "https:") {
|
|
8
|
+
return (process.env.HTTPS_PROXY ||
|
|
9
|
+
process.env.https_proxy ||
|
|
10
|
+
process.env.HTTP_PROXY ||
|
|
11
|
+
process.env.http_proxy);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return process.env.HTTP_PROXY || process.env.http_proxy;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a hostname should bypass proxy based on NO_PROXY environment variable
|
|
19
|
+
* @param hostname The hostname to check
|
|
20
|
+
* @returns True if the hostname should bypass proxy
|
|
21
|
+
*/
|
|
22
|
+
export function shouldBypassProxy(hostname) {
|
|
23
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
24
|
+
if (!noProxy)
|
|
25
|
+
return false;
|
|
26
|
+
const noProxyItems = noProxy.split(",").map((item) => item.trim());
|
|
27
|
+
return noProxyItems.some((item) => {
|
|
28
|
+
// Exact match
|
|
29
|
+
if (item === hostname)
|
|
30
|
+
return true;
|
|
31
|
+
// Wildcard domain match (*.example.com)
|
|
32
|
+
if (item.startsWith("*.") && hostname.endsWith(item.substring(1)))
|
|
33
|
+
return true;
|
|
34
|
+
// Domain suffix match (.example.com)
|
|
35
|
+
if (item.startsWith(".") && hostname.endsWith(item.slice(1)))
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { getProxyFromEnv, shouldBypassProxy } from "./util.js";
|
|
3
|
+
// Reset environment variables after each test
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
jest.resetModules();
|
|
6
|
+
process.env = {};
|
|
7
|
+
});
|
|
8
|
+
// Tests for getProxyFromEnv
|
|
9
|
+
test("getProxyFromEnv returns undefined when no proxy is set", () => {
|
|
10
|
+
expect(getProxyFromEnv("http:")).toBeUndefined();
|
|
11
|
+
expect(getProxyFromEnv("https:")).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
test("getProxyFromEnv returns HTTP_PROXY for http protocol", () => {
|
|
14
|
+
process.env.HTTP_PROXY = "http://proxy.example.com";
|
|
15
|
+
expect(getProxyFromEnv("http:")).toBe("http://proxy.example.com");
|
|
16
|
+
});
|
|
17
|
+
test("getProxyFromEnv returns lowercase http_proxy for http protocol", () => {
|
|
18
|
+
process.env.http_proxy = "http://proxy.example.com";
|
|
19
|
+
expect(getProxyFromEnv("http:")).toBe("http://proxy.example.com");
|
|
20
|
+
});
|
|
21
|
+
test("getProxyFromEnv prefers HTTP_PROXY over http_proxy for http protocol", () => {
|
|
22
|
+
process.env.HTTP_PROXY = "http://upper.example.com";
|
|
23
|
+
process.env.http_proxy = "http://lower.example.com";
|
|
24
|
+
expect(getProxyFromEnv("http:")).toBe("http://upper.example.com");
|
|
25
|
+
});
|
|
26
|
+
test("getProxyFromEnv returns HTTPS_PROXY for https protocol", () => {
|
|
27
|
+
process.env.HTTPS_PROXY = "https://secure.example.com";
|
|
28
|
+
expect(getProxyFromEnv("https:")).toBe("https://secure.example.com");
|
|
29
|
+
});
|
|
30
|
+
test("getProxyFromEnv returns lowercase https_proxy for https protocol", () => {
|
|
31
|
+
process.env.https_proxy = "https://secure.example.com";
|
|
32
|
+
expect(getProxyFromEnv("https:")).toBe("https://secure.example.com");
|
|
33
|
+
});
|
|
34
|
+
test("getProxyFromEnv falls back to HTTP_PROXY for https protocol when HTTPS_PROXY is not set", () => {
|
|
35
|
+
process.env.HTTP_PROXY = "http://fallback.example.com";
|
|
36
|
+
expect(getProxyFromEnv("https:")).toBe("http://fallback.example.com");
|
|
37
|
+
});
|
|
38
|
+
test("getProxyFromEnv prefers HTTPS_PROXY over other env vars for https protocol", () => {
|
|
39
|
+
process.env.HTTPS_PROXY = "https://preferred.example.com";
|
|
40
|
+
process.env.https_proxy = "https://notused1.example.com";
|
|
41
|
+
process.env.HTTP_PROXY = "http://notused2.example.com";
|
|
42
|
+
process.env.http_proxy = "http://notused3.example.com";
|
|
43
|
+
expect(getProxyFromEnv("https:")).toBe("https://preferred.example.com");
|
|
44
|
+
});
|
|
45
|
+
// Tests for shouldBypassProxy
|
|
46
|
+
test("shouldBypassProxy returns false when NO_PROXY is not set", () => {
|
|
47
|
+
expect(shouldBypassProxy("example.com")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
test("shouldBypassProxy returns true for exact hostname match", () => {
|
|
50
|
+
process.env.NO_PROXY = "example.com,another.com";
|
|
51
|
+
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
test("shouldBypassProxy returns false when hostname doesn't match any NO_PROXY entry", () => {
|
|
54
|
+
process.env.NO_PROXY = "example.com,another.com";
|
|
55
|
+
expect(shouldBypassProxy("different.com")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
test("shouldBypassProxy handles lowercase no_proxy", () => {
|
|
58
|
+
process.env.no_proxy = "example.com";
|
|
59
|
+
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
test("shouldBypassProxy works with wildcard domains", () => {
|
|
62
|
+
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);
|
|
66
|
+
});
|
|
67
|
+
test("shouldBypassProxy works with domain suffix", () => {
|
|
68
|
+
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);
|
|
72
|
+
});
|
|
73
|
+
test("shouldBypassProxy handles multiple entries with different patterns", () => {
|
|
74
|
+
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);
|
|
80
|
+
});
|
|
81
|
+
test("shouldBypassProxy ignores whitespace in NO_PROXY", () => {
|
|
82
|
+
process.env.NO_PROXY = " example.com , *.test.org ";
|
|
83
|
+
expect(shouldBypassProxy("example.com")).toBe(true);
|
|
84
|
+
expect(shouldBypassProxy("subdomain.test.org")).toBe(true);
|
|
85
|
+
});
|
package/jest.config.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
transform: {
|
|
6
|
+
"\\.[jt]sx?$": ["ts-jest", { useESM: true }],
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
moduleNameMapper: {
|
|
10
|
+
"(.+)\\.js": "$1",
|
|
11
|
+
},
|
|
12
|
+
extensionsToTreatAsEsm: [".ts"],
|
|
13
|
+
preset: "ts-jest/presets/default-esm",
|
|
14
|
+
testTimeout: 10000,
|
|
15
|
+
testEnvironment: "node",
|
|
16
|
+
globals: {
|
|
17
|
+
__dirname: path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
__filename: path.resolve(fileURLToPath(import.meta.url)),
|
|
19
|
+
},
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuedev/fetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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": "jest",
|
|
9
|
+
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
|
10
10
|
"build": "tsc"
|
|
11
11
|
},
|
|
12
12
|
"author": "Nate Sesti and Ty Dunn",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/follow-redirects": "^1.14.4",
|
|
23
|
-
"jest": "^29.
|
|
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"
|
|
24
28
|
}
|
|
25
29
|
}
|