@continuedev/fetch 1.0.13 → 1.0.15
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 +7 -10
- package/dist/fetch.test.js +0 -1
- package/dist/getAgentOptions.d.ts +2 -2
- package/dist/getAgentOptions.js +13 -40
- package/dist/getAgentOptions.test.js +129 -77
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- 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/util.d.ts +6 -1
- package/dist/util.js +57 -17
- package/dist/util.test.js +92 -18
- package/package.json +1 -1
- package/release.config.js +3 -0
- package/src/certs.test.ts +187 -0
- package/src/certs.ts +129 -0
- package/src/fetch.ts +7 -11
- package/src/getAgentOptions.test.ts +158 -91
- package/src/getAgentOptions.ts +23 -44
- package/src/index.ts +3 -0
- 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.ts +29 -4
- package/src/util.test.ts +130 -18
- package/src/util.ts +84 -19
- package/src/fetch.test.ts +0 -173
package/dist/certs.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts content from either a file path or data URI
|
|
3
|
+
*/
|
|
4
|
+
export declare function getCertificateContent(input: string): string;
|
|
5
|
+
export declare class CertsCache {
|
|
6
|
+
private static instance;
|
|
7
|
+
private _fixedCa;
|
|
8
|
+
private _initialized;
|
|
9
|
+
private _customCerts;
|
|
10
|
+
private constructor();
|
|
11
|
+
static getInstance(): CertsCache;
|
|
12
|
+
get fixedCa(): string[];
|
|
13
|
+
getCachedCustomCert(path: string): Promise<string | undefined>;
|
|
14
|
+
getAllCachedCustomCerts(caBundlePath: string[] | string): Promise<string[]>;
|
|
15
|
+
getCa(caBundlePath: undefined | string | string[]): Promise<string[]>;
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
}
|
package/dist/certs.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { globalAgent } from "https";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import tls from "node:tls";
|
|
4
|
+
/**
|
|
5
|
+
* Extracts content from either a file path or data URI
|
|
6
|
+
*/
|
|
7
|
+
export function getCertificateContent(input) {
|
|
8
|
+
if (input.startsWith("data:")) {
|
|
9
|
+
// Parse data URI: data:[<mediatype>][;base64],<data>
|
|
10
|
+
const [header, data] = input.split(",");
|
|
11
|
+
if (header.includes("base64")) {
|
|
12
|
+
return Buffer.from(data, "base64").toString("utf8");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return decodeURIComponent(data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Assume it's a file path
|
|
20
|
+
return fs.readFileSync(input, "utf8");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class CertsCache {
|
|
24
|
+
static instance;
|
|
25
|
+
_fixedCa = [];
|
|
26
|
+
_initialized = false;
|
|
27
|
+
_customCerts = new Map();
|
|
28
|
+
constructor() { }
|
|
29
|
+
static getInstance() {
|
|
30
|
+
if (!CertsCache.instance) {
|
|
31
|
+
CertsCache.instance = new CertsCache();
|
|
32
|
+
}
|
|
33
|
+
return CertsCache.instance;
|
|
34
|
+
}
|
|
35
|
+
get fixedCa() {
|
|
36
|
+
if (this._initialized) {
|
|
37
|
+
return this._fixedCa;
|
|
38
|
+
}
|
|
39
|
+
const globalCerts = [];
|
|
40
|
+
if (Boolean(process.env.IS_BINARY)) {
|
|
41
|
+
if (Array.isArray(globalAgent.options.ca)) {
|
|
42
|
+
globalCerts.push(...globalAgent.options.ca.map((cert) => cert.toString()));
|
|
43
|
+
}
|
|
44
|
+
else if (typeof globalAgent.options.ca !== "undefined") {
|
|
45
|
+
globalCerts.push(globalAgent.options.ca.toString());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const extraCerts = [];
|
|
49
|
+
if (process.env.NODE_EXTRA_CA_CERTS) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(process.env.NODE_EXTRA_CA_CERTS, "utf8");
|
|
52
|
+
extraCerts.push(content);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (process.env.VERBOSE_FETCH) {
|
|
56
|
+
console.error(`Error reading NODE_EXTRA_CA_CERTS file: ${process.env.NODE_EXTRA_CA_CERTS}`, error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this._fixedCa = Array.from(new Set([...tls.rootCertificates, ...globalCerts, ...extraCerts]));
|
|
61
|
+
this._initialized = true;
|
|
62
|
+
return this._fixedCa;
|
|
63
|
+
}
|
|
64
|
+
async getCachedCustomCert(path) {
|
|
65
|
+
if (this._customCerts.has(path)) {
|
|
66
|
+
return this._customCerts.get(path);
|
|
67
|
+
}
|
|
68
|
+
const certContent = getCertificateContent(path);
|
|
69
|
+
this._customCerts.set(path, certContent);
|
|
70
|
+
return certContent;
|
|
71
|
+
}
|
|
72
|
+
async getAllCachedCustomCerts(caBundlePath) {
|
|
73
|
+
const paths = Array.isArray(caBundlePath) ? caBundlePath : [caBundlePath];
|
|
74
|
+
const certs = [];
|
|
75
|
+
await Promise.all(paths.map(async (path) => {
|
|
76
|
+
try {
|
|
77
|
+
const certContent = await this.getCachedCustomCert(path);
|
|
78
|
+
if (certContent) {
|
|
79
|
+
certs.push(certContent);
|
|
80
|
+
}
|
|
81
|
+
else if (process.env.VERBOSE_FETCH) {
|
|
82
|
+
console.warn(`Empty certificate found at ${path}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (process.env.VERBOSE_FETCH) {
|
|
87
|
+
console.error(`Error loading custom certificate from ${path}:`, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
return certs;
|
|
92
|
+
}
|
|
93
|
+
async getCa(caBundlePath) {
|
|
94
|
+
if (!caBundlePath) {
|
|
95
|
+
return this.fixedCa;
|
|
96
|
+
}
|
|
97
|
+
const customCerts = await this.getAllCachedCustomCerts(caBundlePath);
|
|
98
|
+
return [...this.fixedCa, ...customCerts];
|
|
99
|
+
}
|
|
100
|
+
async clear() {
|
|
101
|
+
this._customCerts.clear();
|
|
102
|
+
this._initialized = false;
|
|
103
|
+
this._fixedCa = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { beforeEach, expect, test, vi } from "vitest";
|
|
3
|
+
import { CertsCache, getCertificateContent } from "./certs.js";
|
|
4
|
+
// Mock fs module
|
|
5
|
+
vi.mock("node:fs", () => ({
|
|
6
|
+
readFileSync: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
const mockReadFileSync = vi.mocked(fs.readFileSync);
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
test("getCertificateContent should decode base64 data URI correctly", () => {
|
|
13
|
+
const testCert = "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----";
|
|
14
|
+
const base64Data = Buffer.from(testCert, "utf8").toString("base64");
|
|
15
|
+
const dataUri = `data:application/x-pem-file;base64,${base64Data}`;
|
|
16
|
+
const result = getCertificateContent(dataUri);
|
|
17
|
+
expect(result).toBe(testCert);
|
|
18
|
+
});
|
|
19
|
+
test("getCertificateContent should decode URL-encoded data URI correctly", () => {
|
|
20
|
+
const testCert = "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----";
|
|
21
|
+
const encodedData = encodeURIComponent(testCert);
|
|
22
|
+
const dataUri = `data:text/plain,${encodedData}`;
|
|
23
|
+
const result = getCertificateContent(dataUri);
|
|
24
|
+
expect(result).toBe(testCert);
|
|
25
|
+
});
|
|
26
|
+
test("getCertificateContent should handle plain text data URI correctly", () => {
|
|
27
|
+
const testCert = "simple-cert-content";
|
|
28
|
+
const dataUri = `data:text/plain,${testCert}`;
|
|
29
|
+
const result = getCertificateContent(dataUri);
|
|
30
|
+
expect(result).toBe(testCert);
|
|
31
|
+
});
|
|
32
|
+
test("getCertificateContent should read file when input is a file path", () => {
|
|
33
|
+
const filePath = "/path/to/cert.pem";
|
|
34
|
+
const expectedContent = "-----BEGIN CERTIFICATE-----\nfile content\n-----END CERTIFICATE-----";
|
|
35
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
36
|
+
const result = getCertificateContent(filePath);
|
|
37
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
38
|
+
expect(result).toBe(expectedContent);
|
|
39
|
+
});
|
|
40
|
+
test("getCertificateContent should handle data URI with different media types", () => {
|
|
41
|
+
const testData = "certificate-data";
|
|
42
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
43
|
+
const dataUri = `data:application/x-x509-ca-cert;base64,${base64Data}`;
|
|
44
|
+
const result = getCertificateContent(dataUri);
|
|
45
|
+
expect(result).toBe(testData);
|
|
46
|
+
});
|
|
47
|
+
test("getCertificateContent should handle data URI without media type", () => {
|
|
48
|
+
const testData = "simple-data";
|
|
49
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
50
|
+
const dataUri = `data:;base64,${base64Data}`;
|
|
51
|
+
const result = getCertificateContent(dataUri);
|
|
52
|
+
expect(result).toBe(testData);
|
|
53
|
+
});
|
|
54
|
+
test("getCertificateContent should handle data URI without media type or encoding", () => {
|
|
55
|
+
const testData = "simple-data";
|
|
56
|
+
const base64Data = Buffer.from(testData, "utf8").toString("base64");
|
|
57
|
+
const dataUri = `data:;base64,${base64Data}`;
|
|
58
|
+
const result = getCertificateContent(dataUri);
|
|
59
|
+
expect(result).toBe(testData);
|
|
60
|
+
});
|
|
61
|
+
test("getCertificateContent should handle relative file paths", () => {
|
|
62
|
+
const filePath = "./certs/ca.pem";
|
|
63
|
+
const expectedContent = "certificate from relative path";
|
|
64
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
65
|
+
const result = getCertificateContent(filePath);
|
|
66
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
67
|
+
expect(result).toBe(expectedContent);
|
|
68
|
+
});
|
|
69
|
+
test("getCertificateContent should handle data URI with special characters in URL encoding", () => {
|
|
70
|
+
const testCert = "cert with spaces and special chars: !@#$%";
|
|
71
|
+
const encodedData = encodeURIComponent(testCert);
|
|
72
|
+
const dataUri = `data:text/plain,${encodedData}`;
|
|
73
|
+
const result = getCertificateContent(dataUri);
|
|
74
|
+
expect(result).toBe(testCert);
|
|
75
|
+
});
|
|
76
|
+
test("CertsCache.getCachedCustomCert should cache and return certificate content", async () => {
|
|
77
|
+
const certsCache = CertsCache.getInstance();
|
|
78
|
+
const filePath = "/path/to/custom/cert.pem";
|
|
79
|
+
const expectedContent = "custom cert content";
|
|
80
|
+
mockReadFileSync.mockReturnValue(expectedContent);
|
|
81
|
+
const cert1 = await certsCache.getCachedCustomCert(filePath);
|
|
82
|
+
expect(cert1).toBe(expectedContent);
|
|
83
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(filePath, "utf8");
|
|
85
|
+
// Call again to check if it's cached
|
|
86
|
+
const cert2 = await certsCache.getCachedCustomCert(filePath);
|
|
87
|
+
expect(cert2).toBe(expectedContent);
|
|
88
|
+
// readFileSync should not be called again
|
|
89
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
|
|
90
|
+
});
|
|
91
|
+
test("CertsCache.getAllCachedCustomCerts should return all cached custom certs", async () => {
|
|
92
|
+
const certsCache = CertsCache.getInstance();
|
|
93
|
+
const filePaths = ["/path/to/cert1.pem", "/path/to/cert2.pem"];
|
|
94
|
+
const expectedContent1 = "content of cert1";
|
|
95
|
+
const expectedContent2 = "content of cert2";
|
|
96
|
+
mockReadFileSync.mockReturnValueOnce(expectedContent1);
|
|
97
|
+
mockReadFileSync.mockReturnValueOnce(expectedContent2);
|
|
98
|
+
const certs = await certsCache.getAllCachedCustomCerts(filePaths);
|
|
99
|
+
expect(certs).toEqual([expectedContent1, expectedContent2]);
|
|
100
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
|
|
101
|
+
});
|
|
102
|
+
test("CertsCache.getCa should return combined CA when caBundlePath is provided", async () => {
|
|
103
|
+
const certsCache = CertsCache.getInstance();
|
|
104
|
+
const fixedCa = ["fixed CA cert"];
|
|
105
|
+
const customCertPath = "/path/to/custom/cert.pem";
|
|
106
|
+
const customCertContent = "custom cert content";
|
|
107
|
+
// Directly set _fixedCa to avoid initializing it with real data
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
certsCache._fixedCa = fixedCa;
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
certsCache._initialized = true;
|
|
114
|
+
mockReadFileSync.mockReturnValue(customCertContent);
|
|
115
|
+
const ca = await certsCache.getCa(customCertPath);
|
|
116
|
+
expect(ca).toEqual([...fixedCa, customCertContent]);
|
|
117
|
+
});
|
|
118
|
+
test("CertsCache.clear should clear custom certs and reset initialized flag", () => {
|
|
119
|
+
const certsCache = CertsCache.getInstance();
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
121
|
+
// @ts-ignore
|
|
122
|
+
certsCache._customCerts.set("key", "value");
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
124
|
+
// @ts-ignore
|
|
125
|
+
certsCache._initialized = true;
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
127
|
+
// @ts-ignore
|
|
128
|
+
certsCache._fixedCa = ["test"];
|
|
129
|
+
certsCache.clear();
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
expect(certsCache._customCerts.size).toBe(0);
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
134
|
+
// @ts-ignore
|
|
135
|
+
expect(certsCache._initialized).toBe(false);
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
expect(certsCache._fixedCa).toEqual([]);
|
|
139
|
+
});
|
package/dist/fetch.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as followRedirects from "follow-redirects";
|
|
2
2
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
3
3
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
4
|
-
import
|
|
4
|
+
import { Response } from "node-fetch";
|
|
5
5
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
6
|
-
import
|
|
6
|
+
import patchedFetch from "./node-fetch-patch.js";
|
|
7
|
+
import { getProxy, shouldBypassProxy } from "./util.js";
|
|
7
8
|
const { http, https } = followRedirects.default;
|
|
8
9
|
function logRequest(method, url, headers, body, proxy, shouldBypass) {
|
|
9
10
|
console.log("=== FETCH REQUEST ===");
|
|
@@ -65,15 +66,11 @@ export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
|
65
66
|
if (url.host === "localhost") {
|
|
66
67
|
url.host = "127.0.0.1";
|
|
67
68
|
}
|
|
68
|
-
const agentOptions = getAgentOptions(requestOptions);
|
|
69
|
+
const agentOptions = await getAgentOptions(requestOptions);
|
|
69
70
|
// Get proxy from options or environment variables
|
|
70
|
-
|
|
71
|
-
if (!proxy) {
|
|
72
|
-
proxy = getProxyFromEnv(url.protocol);
|
|
73
|
-
}
|
|
71
|
+
const proxy = getProxy(url.protocol, requestOptions);
|
|
74
72
|
// Check if should bypass proxy based on requestOptions or NO_PROXY env var
|
|
75
|
-
const shouldBypass =
|
|
76
|
-
shouldBypassProxy(url.hostname);
|
|
73
|
+
const shouldBypass = shouldBypassProxy(url.hostname, requestOptions);
|
|
77
74
|
// Create agent
|
|
78
75
|
const protocol = url.protocol === "https:" ? https : http;
|
|
79
76
|
const agent = proxy && !shouldBypass
|
|
@@ -115,7 +112,7 @@ export async function fetchwithRequestOptions(url_, init, requestOptions) {
|
|
|
115
112
|
}
|
|
116
113
|
// fetch the request with the provided options
|
|
117
114
|
try {
|
|
118
|
-
const resp = await
|
|
115
|
+
const resp = await patchedFetch(url, {
|
|
119
116
|
...init,
|
|
120
117
|
body: finalBody,
|
|
121
118
|
headers: headers,
|
package/dist/fetch.test.js
CHANGED
|
@@ -2,7 +2,6 @@ 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
5
|
import { getAgentOptions } from "./getAgentOptions.js";
|
|
7
6
|
// Store original env
|
|
8
7
|
const originalEnv = process.env;
|
|
@@ -2,6 +2,6 @@ import { RequestOptions } from "@continuedev/config-types";
|
|
|
2
2
|
/**
|
|
3
3
|
* Prepares agent options based on request options and certificates
|
|
4
4
|
*/
|
|
5
|
-
export declare function getAgentOptions(requestOptions?: RequestOptions): {
|
|
5
|
+
export declare function getAgentOptions(requestOptions?: RequestOptions): Promise<{
|
|
6
6
|
[key: string]: any;
|
|
7
|
-
}
|
|
7
|
+
}>;
|
package/dist/getAgentOptions.js
CHANGED
|
@@ -1,48 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import tls from "node:tls";
|
|
4
|
-
/**
|
|
5
|
-
* Extracts content from either a file path or data URI
|
|
6
|
-
*/
|
|
7
|
-
function getCertificateContent(input) {
|
|
8
|
-
if (input.startsWith("data:")) {
|
|
9
|
-
// Parse data URI: data:[<mediatype>][;base64],<data>
|
|
10
|
-
const [header, data] = input.split(",");
|
|
11
|
-
if (header.includes("base64")) {
|
|
12
|
-
return Buffer.from(data, "base64").toString("utf8");
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
return decodeURIComponent(data);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
// Assume it's a file path
|
|
20
|
-
return fs.readFileSync(input, "utf8");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
1
|
+
import { CertsCache, getCertificateContent } from "./certs.js";
|
|
23
2
|
/**
|
|
24
3
|
* Prepares agent options based on request options and certificates
|
|
25
4
|
*/
|
|
26
|
-
export function getAgentOptions(requestOptions) {
|
|
5
|
+
export async function getAgentOptions(requestOptions) {
|
|
27
6
|
const TIMEOUT = 7200; // 7200 seconds = 2 hours
|
|
28
7
|
const timeout = (requestOptions?.timeout ?? TIMEOUT) * 1000; // measured in ms
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (process.env.IS_BINARY) {
|
|
32
|
-
if (Array.isArray(globalAgent.options.ca)) {
|
|
33
|
-
globalCerts = [...globalAgent.options.ca.map((cert) => cert.toString())];
|
|
34
|
-
}
|
|
35
|
-
else if (typeof globalAgent.options.ca !== "undefined") {
|
|
36
|
-
globalCerts.push(globalAgent.options.ca.toString());
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const ca = Array.from(new Set([...tls.rootCertificates, ...globalCerts]));
|
|
40
|
-
const customCerts = typeof requestOptions?.caBundlePath === "string"
|
|
41
|
-
? [requestOptions?.caBundlePath]
|
|
42
|
-
: requestOptions?.caBundlePath;
|
|
43
|
-
if (customCerts) {
|
|
44
|
-
ca.push(...customCerts.map((customCert) => getCertificateContent(customCert)));
|
|
45
|
-
}
|
|
8
|
+
const certsCache = CertsCache.getInstance();
|
|
9
|
+
const ca = await certsCache.getCa(requestOptions?.caBundlePath);
|
|
46
10
|
const agentOptions = {
|
|
47
11
|
ca,
|
|
48
12
|
rejectUnauthorized: requestOptions?.verifySsl,
|
|
@@ -60,5 +24,14 @@ export function getAgentOptions(requestOptions) {
|
|
|
60
24
|
agentOptions.passphrase = passphrase;
|
|
61
25
|
}
|
|
62
26
|
}
|
|
27
|
+
if (process.env.VERBOSE_FETCH) {
|
|
28
|
+
console.log(`Fetch agent options:`);
|
|
29
|
+
console.log(`\tTimeout (sessionTimeout/keepAliveMsecs): ${agentOptions.timeout}`);
|
|
30
|
+
console.log(`\tTotal CA certs: ${ca.length}`);
|
|
31
|
+
console.log(`\tGlobal/Root CA certs: ${certsCache.fixedCa.length}`);
|
|
32
|
+
console.log(`\tCustom CA certs: ${ca.length - certsCache.fixedCa.length}`);
|
|
33
|
+
console.log(`\tClient certificate: ${requestOptions?.clientCertificate ? "Yes" : "No"}`);
|
|
34
|
+
console.log(`\trejectUnauthorized/verifySsl: ${agentOptions.rejectUnauthorized ?? "not set (defaults to true)"}`);
|
|
35
|
+
}
|
|
63
36
|
return agentOptions;
|
|
64
37
|
}
|
|
@@ -1,89 +1,141 @@
|
|
|
1
|
+
import { globalAgent } from "https";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, expect, test } from "vitest";
|
|
6
|
+
import { CertsCache } from "./certs.js";
|
|
7
|
+
import { getAgentOptions } from "./getAgentOptions.js";
|
|
8
|
+
// Store original env
|
|
9
|
+
const originalEnv = process.env;
|
|
10
|
+
const originalGlobalAgentOptions = { ...globalAgent.options };
|
|
11
|
+
// Temporary directory for test certificate files
|
|
12
|
+
let tempDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create a temporary directory for test files
|
|
15
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fetch-test-"));
|
|
16
|
+
process.env = { ...originalEnv };
|
|
17
|
+
// Reset globalAgent for each test
|
|
18
|
+
globalAgent.options = { ...originalGlobalAgentOptions };
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env = originalEnv;
|
|
22
|
+
globalAgent.options = originalGlobalAgentOptions;
|
|
23
|
+
CertsCache.getInstance().clear();
|
|
24
|
+
// Clean up temporary directory
|
|
25
|
+
try {
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
27
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return fs.readFileSync(input, "utf8");
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(`Failed to remove temp directory: ${error}`);
|
|
24
30
|
}
|
|
25
|
-
};
|
|
26
|
-
test("getCertificateContent should decode base64 data URI correctly", () => {
|
|
27
|
-
const testCert = "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----";
|
|
28
|
-
const base64Data = Buffer.from(testCert, "utf8").toString("base64");
|
|
29
|
-
const dataUri = `data:application/x-pem-file;base64,${base64Data}`;
|
|
30
|
-
const result = getCertificateContent(dataUri);
|
|
31
|
-
expect(result).toBe(testCert);
|
|
32
31
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// Helper function to create test certificate files
|
|
33
|
+
function createTestCertFile(filename, content) {
|
|
34
|
+
const filePath = path.join(tempDir, filename);
|
|
35
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
36
|
+
return filePath;
|
|
37
|
+
}
|
|
38
|
+
test("getAgentOptions returns basic configuration with default values", async () => {
|
|
39
|
+
const options = await getAgentOptions();
|
|
40
|
+
// Check default timeout (7200 seconds = 2 hours = 7,200,000 ms)
|
|
41
|
+
expect(options.timeout).toBe(7200000);
|
|
42
|
+
expect(options.sessionTimeout).toBe(7200000);
|
|
43
|
+
expect(options.keepAliveMsecs).toBe(7200000);
|
|
44
|
+
expect(options.keepAlive).toBe(true);
|
|
45
|
+
// Verify certificates array exists and contains items
|
|
46
|
+
expect(options.ca).toBeInstanceOf(Array);
|
|
47
|
+
expect(options.ca.length).toBeGreaterThan(0);
|
|
48
|
+
// Verify at least one of the real TLS root certificates is included
|
|
49
|
+
// This assumes there's at least one certificate with "CERTIFICATE" in it
|
|
50
|
+
expect(options.ca.some((cert) => cert.includes("CERTIFICATE"))).toBe(true);
|
|
39
51
|
});
|
|
40
|
-
test("
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
expect(
|
|
52
|
+
test("getAgentOptions respects custom timeout", async () => {
|
|
53
|
+
const customTimeout = 300; // 5 minutes
|
|
54
|
+
const options = await getAgentOptions({ timeout: customTimeout });
|
|
55
|
+
// Check timeout values (300 seconds = 300,000 ms)
|
|
56
|
+
expect(options.timeout).toBe(300000);
|
|
57
|
+
expect(options.sessionTimeout).toBe(300000);
|
|
58
|
+
expect(options.keepAliveMsecs).toBe(300000);
|
|
45
59
|
});
|
|
46
|
-
test("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expect(
|
|
60
|
+
test("getAgentOptions uses verifySsl setting", async () => {
|
|
61
|
+
// With verifySsl true
|
|
62
|
+
let options = await getAgentOptions({ verifySsl: true });
|
|
63
|
+
expect(options.rejectUnauthorized).toBe(true);
|
|
64
|
+
// With verifySsl false
|
|
65
|
+
options = await getAgentOptions({ verifySsl: false });
|
|
66
|
+
expect(options.rejectUnauthorized).toBe(false);
|
|
53
67
|
});
|
|
54
|
-
test("
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
test("getAgentOptions incorporates custom CA bundle paths", async () => {
|
|
69
|
+
// Create a test CA bundle file
|
|
70
|
+
const caBundleContent = "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIJAMcuSp7chAYdMA==\n-----END CERTIFICATE-----";
|
|
71
|
+
const caBundlePath = createTestCertFile("ca-bundle.pem", caBundleContent);
|
|
72
|
+
// Single string path
|
|
73
|
+
let options = await getAgentOptions({ caBundlePath });
|
|
74
|
+
// Verify that our test certificate is included in the CA list
|
|
75
|
+
expect(options.ca).toContain(caBundleContent);
|
|
76
|
+
// Create multiple test CA bundle files
|
|
77
|
+
const caContent1 = "-----BEGIN CERTIFICATE-----\nABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n-----END CERTIFICATE-----";
|
|
78
|
+
const caContent2 = "-----BEGIN CERTIFICATE-----\n0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\n-----END CERTIFICATE-----";
|
|
79
|
+
const caPath1 = createTestCertFile("ca1.pem", caContent1);
|
|
80
|
+
const caPath2 = createTestCertFile("ca2.pem", caContent2);
|
|
81
|
+
// Array of paths
|
|
82
|
+
options = await getAgentOptions({
|
|
83
|
+
caBundlePath: [caPath1, caPath2],
|
|
84
|
+
});
|
|
85
|
+
// Verify that both test certificates are included in the CA list
|
|
86
|
+
expect(options.ca).toContain(caContent1);
|
|
87
|
+
expect(options.ca).toContain(caContent2);
|
|
60
88
|
});
|
|
61
|
-
test("
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
test("getAgentOptions includes global certs when running as binary", async () => {
|
|
90
|
+
// Set up test certs in globalAgent
|
|
91
|
+
globalAgent.options.ca = ["global-cert-1", "global-cert-2"];
|
|
92
|
+
// Set IS_BINARY environment variable
|
|
93
|
+
process.env.IS_BINARY = "true";
|
|
94
|
+
const options = await getAgentOptions();
|
|
95
|
+
// Test for global certs
|
|
96
|
+
expect(options.ca).toContain("global-cert-1");
|
|
97
|
+
expect(options.ca).toContain("global-cert-2");
|
|
67
98
|
});
|
|
68
|
-
test("
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
99
|
+
test("getAgentOptions handles client certificate configuration", async () => {
|
|
100
|
+
// Create test certificate files
|
|
101
|
+
const clientCertContent = "-----BEGIN CERTIFICATE-----\nCLIENTCERT\n-----END CERTIFICATE-----";
|
|
102
|
+
const clientKeyContent = "-----BEGIN PRIVATE KEY-----\nCLIENTKEY\n-----END PRIVATE KEY-----";
|
|
103
|
+
const certPath = createTestCertFile("client.cert", clientCertContent);
|
|
104
|
+
const keyPath = createTestCertFile("client.key", clientKeyContent);
|
|
105
|
+
const clientCertOptions = {
|
|
106
|
+
clientCertificate: {
|
|
107
|
+
cert: certPath,
|
|
108
|
+
key: keyPath,
|
|
109
|
+
passphrase: "secret-passphrase",
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const options = await getAgentOptions(clientCertOptions);
|
|
113
|
+
expect(options.cert).toBe(clientCertContent);
|
|
114
|
+
expect(options.key).toBe(clientKeyContent);
|
|
115
|
+
expect(options.passphrase).toBe("secret-passphrase");
|
|
74
116
|
});
|
|
75
|
-
test("
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
test("getAgentOptions handles client certificate without passphrase", async () => {
|
|
118
|
+
// Create test certificate files
|
|
119
|
+
const clientCertContent = "-----BEGIN CERTIFICATE-----\nCLIENTCERT2\n-----END CERTIFICATE-----";
|
|
120
|
+
const clientKeyContent = "-----BEGIN PRIVATE KEY-----\nCLIENTKEY2\n-----END PRIVATE KEY-----";
|
|
121
|
+
const certPath = createTestCertFile("client2.cert", clientCertContent);
|
|
122
|
+
const keyPath = createTestCertFile("client2.key", clientKeyContent);
|
|
123
|
+
const clientCertOptions = {
|
|
124
|
+
clientCertificate: {
|
|
125
|
+
cert: certPath,
|
|
126
|
+
key: keyPath,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const options = await getAgentOptions(clientCertOptions);
|
|
130
|
+
expect(options.cert).toBe(clientCertContent);
|
|
131
|
+
expect(options.key).toBe(clientKeyContent);
|
|
132
|
+
expect(options.passphrase).toBeUndefined();
|
|
82
133
|
});
|
|
83
|
-
test("
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
test("getAgentOptions reads NODE_EXTRA_CA_CERTS", async () => {
|
|
135
|
+
const extraCertContent = "-----BEGIN CERTIFICATE-----\nEXTRA_CERT\n-----END CERTIFICATE-----";
|
|
136
|
+
const certPath = createTestCertFile("extra-cert.cert", extraCertContent);
|
|
137
|
+
expect(CertsCache.getInstance().fixedCa).not.toContain(extraCertContent);
|
|
138
|
+
CertsCache.getInstance().clear();
|
|
139
|
+
process.env.NODE_EXTRA_CA_CERTS = certPath;
|
|
140
|
+
expect(CertsCache.getInstance().fixedCa).toContain(extraCertContent);
|
|
89
141
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { streamJSON, streamResponse, streamSse, toAsyncIterable } from "./stream.js";
|
|
2
|
+
import patchedFetch from "./node-fetch-patch.js";
|
|
2
3
|
import { fetchwithRequestOptions } from "./fetch.js";
|
|
3
|
-
export { fetchwithRequestOptions, streamJSON, streamResponse, streamSse, toAsyncIterable, };
|
|
4
|
+
export { fetchwithRequestOptions, patchedFetch, streamJSON, streamResponse, streamSse, toAsyncIterable, };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { streamJSON, streamResponse, streamSse, toAsyncIterable, } from "./stream.js";
|
|
2
|
+
import patchedFetch from "./node-fetch-patch.js";
|
|
2
3
|
import { fetchwithRequestOptions } from "./fetch.js";
|
|
3
|
-
export { fetchwithRequestOptions, streamJSON, streamResponse, streamSse, toAsyncIterable, };
|
|
4
|
+
export { fetchwithRequestOptions, patchedFetch, streamJSON, streamResponse, streamSse, toAsyncIterable, };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch function
|
|
3
|
+
*
|
|
4
|
+
* @param {string | URL | import('./request').default} url - Absolute url or Request instance
|
|
5
|
+
* @param {*} [options_] - Fetch options
|
|
6
|
+
* @return {Promise<import('./response').default>}
|
|
7
|
+
*/
|
|
8
|
+
export default function fetch(url: string | URL | any, options_?: any): Promise<any>;
|
|
9
|
+
import { Blob } from "fetch-blob/from.js";
|
|
10
|
+
import { blobFrom } from "fetch-blob/from.js";
|
|
11
|
+
import { blobFromSync } from "fetch-blob/from.js";
|
|
12
|
+
import { File } from "fetch-blob/from.js";
|
|
13
|
+
import { fileFrom } from "fetch-blob/from.js";
|
|
14
|
+
import { fileFromSync } from "fetch-blob/from.js";
|
|
15
|
+
import { FormData } from "formdata-polyfill/esm.min.js";
|
|
16
|
+
export { AbortError, Blob, blobFrom, blobFromSync, FetchError, File, fileFrom, fileFromSync, FormData, Headers, isRedirect, Request, Response };
|