@continuedev/fetch 1.0.15 → 1.1.0
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/CHANGELOG.md +1177 -0
- package/dist/fetch.e2e.test.js +356 -0
- package/dist/fetch.js +22 -2
- package/dist/ssl-certificate.test.d.ts +1 -0
- package/dist/ssl-certificate.test.js +231 -0
- package/dist/stream.js +7 -1
- package/dist/stream.test.js +5 -1
- package/package.json +9 -3
- package/src/fetch.e2e.test.ts +518 -0
- package/src/fetch.ts +23 -2
- package/src/ssl-certificate.test.ts +301 -0
- package/src/stream.test.ts +8 -1
- package/src/stream.ts +11 -1
- package/vitest.config.ts +3 -0
- package/dist/fetch.test.js +0 -130
- package/src/node_modules/.vite/vitest/d41d8cd98f00b204e9800998ecf8427e/results.json +0 -1
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- /package/dist/{fetch.test.d.ts → fetch.e2e.test.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuedev/fetch",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"author": "Nate Sesti and Ty Dunn",
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@continuedev/config-types": "^1.0.
|
|
15
|
+
"@continuedev/config-types": "^1.0.14",
|
|
16
16
|
"follow-redirects": "^1.15.6",
|
|
17
17
|
"http-proxy-agent": "^7.0.2",
|
|
18
18
|
"https-proxy-agent": "^7.0.5",
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/follow-redirects": "^1.14.4",
|
|
23
|
-
"
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"vitest": "^3.2.0",
|
|
25
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
26
|
+
"@semantic-release/git": "^10.0.1",
|
|
27
|
+
"@semantic-release/github": "^9.2.6",
|
|
28
|
+
"@semantic-release/npm": "^10.0.6",
|
|
29
|
+
"semantic-release": "^21.1.2"
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as https from "node:https";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
8
|
+
import { fetchwithRequestOptions } from "./fetch.js";
|
|
9
|
+
|
|
10
|
+
// Test server ports
|
|
11
|
+
const HTTP_PORT = 3001;
|
|
12
|
+
const HTTPS_PORT = 3002;
|
|
13
|
+
|
|
14
|
+
// Track servers and temp dirs for cleanup
|
|
15
|
+
const serversToCleanup: Array<http.Server | https.Server> = [];
|
|
16
|
+
const tempDirsToCleanup: string[] = [];
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Clean up all servers
|
|
20
|
+
serversToCleanup.forEach((server) => server.close());
|
|
21
|
+
serversToCleanup.length = 0;
|
|
22
|
+
|
|
23
|
+
// Clean up temp directories
|
|
24
|
+
tempDirsToCleanup.forEach((tempDir) => {
|
|
25
|
+
if (fs.existsSync(tempDir)) {
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
tempDirsToCleanup.length = 0;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a temporary directory for test files
|
|
34
|
+
*/
|
|
35
|
+
function createTempDir(): string {
|
|
36
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fetch-e2e-test-"));
|
|
37
|
+
tempDirsToCleanup.push(tempDir);
|
|
38
|
+
return tempDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a self-signed certificate for testing
|
|
43
|
+
*/
|
|
44
|
+
async function generateCertificate(
|
|
45
|
+
tempDir: string,
|
|
46
|
+
name: string,
|
|
47
|
+
options: { cn: string; san: string[]; org: string },
|
|
48
|
+
): Promise<{ certPath: string; keyPath: string; caCertPath: string }> {
|
|
49
|
+
const certPath = path.join(tempDir, `${name}.crt`);
|
|
50
|
+
const keyPath = path.join(tempDir, `${name}.key`);
|
|
51
|
+
const caCertPath = path.join(tempDir, `${name}-ca.crt`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Generate a private key
|
|
55
|
+
execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: "pipe" });
|
|
56
|
+
|
|
57
|
+
// Create config file for certificate
|
|
58
|
+
const configPath = path.join(tempDir, `${name}.conf`);
|
|
59
|
+
const altNames = options.san
|
|
60
|
+
.map((san, i) => {
|
|
61
|
+
return san.match(/^\d+\.\d+\.\d+\.\d+$/)
|
|
62
|
+
? `IP.${i + 1} = ${san}`
|
|
63
|
+
: `DNS.${i + 1} = ${san}`;
|
|
64
|
+
})
|
|
65
|
+
.join("\n");
|
|
66
|
+
|
|
67
|
+
const configContent = `
|
|
68
|
+
[req]
|
|
69
|
+
distinguished_name = req_distinguished_name
|
|
70
|
+
req_extensions = v3_req
|
|
71
|
+
prompt = no
|
|
72
|
+
|
|
73
|
+
[req_distinguished_name]
|
|
74
|
+
C = US
|
|
75
|
+
ST = Test
|
|
76
|
+
L = Test
|
|
77
|
+
O = ${options.org}
|
|
78
|
+
CN = ${options.cn}
|
|
79
|
+
|
|
80
|
+
[v3_req]
|
|
81
|
+
keyUsage = keyEncipherment, dataEncipherment
|
|
82
|
+
extendedKeyUsage = serverAuth
|
|
83
|
+
subjectAltName = @alt_names
|
|
84
|
+
|
|
85
|
+
[alt_names]
|
|
86
|
+
${altNames}
|
|
87
|
+
`;
|
|
88
|
+
fs.writeFileSync(configPath, configContent);
|
|
89
|
+
|
|
90
|
+
// Generate a self-signed certificate
|
|
91
|
+
execSync(
|
|
92
|
+
`openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -config "${configPath}" -extensions v3_req`,
|
|
93
|
+
{ stdio: "pipe" },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Copy the certificate as CA (for testing custom CA scenarios)
|
|
97
|
+
fs.copyFileSync(certPath, caCertPath);
|
|
98
|
+
|
|
99
|
+
return { certPath, keyPath, caCertPath };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// Fallback: create minimal test certificates if OpenSSL not available
|
|
102
|
+
const key = `-----BEGIN PRIVATE KEY-----
|
|
103
|
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7VJTUt9Us8cKB
|
|
104
|
+
wEiOfniel+2jNcJjYUiUoq5YbVKk+xqt4bOMh5DNFJ3LnU1OaUHyG5sHlgNyKA==
|
|
105
|
+
-----END PRIVATE KEY-----`;
|
|
106
|
+
|
|
107
|
+
const cert = `-----BEGIN CERTIFICATE-----
|
|
108
|
+
MIICljCCAX4CCQCKnW9qX7TlxzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV
|
|
109
|
+
UzAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMA0xCzAJBgNVBAYTAlVT
|
|
110
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1L7VLPHCQD1OvF4p
|
|
111
|
+
4td9ozXCY2FIlKKuWG1SpPsareGzjIeQzRSdy51NTmlB8hubB5YDcigN8=
|
|
112
|
+
-----END CERTIFICATE-----`;
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(keyPath, key);
|
|
115
|
+
fs.writeFileSync(certPath, cert);
|
|
116
|
+
fs.writeFileSync(caCertPath, cert);
|
|
117
|
+
|
|
118
|
+
return { certPath, keyPath, caCertPath };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start a simple HTTP server for testing
|
|
124
|
+
*/
|
|
125
|
+
async function startHttpServer(): Promise<http.Server> {
|
|
126
|
+
const server = http.createServer((req, res) => {
|
|
127
|
+
const url = new URL(req.url!, `http://localhost:${HTTP_PORT}`);
|
|
128
|
+
|
|
129
|
+
if (url.pathname === "/json") {
|
|
130
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
message: "Hello from HTTP server",
|
|
134
|
+
method: req.method,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
} else if (url.pathname === "/headers") {
|
|
138
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
139
|
+
res.end(JSON.stringify({ headers: req.headers }));
|
|
140
|
+
} else if (url.pathname === "/body") {
|
|
141
|
+
let body = "";
|
|
142
|
+
req.on("data", (chunk) => {
|
|
143
|
+
body += chunk.toString();
|
|
144
|
+
});
|
|
145
|
+
req.on("end", () => {
|
|
146
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
147
|
+
res.end(JSON.stringify({ body, headers: req.headers }));
|
|
148
|
+
});
|
|
149
|
+
} else if (url.pathname === "/status/404") {
|
|
150
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
151
|
+
res.end("Not Found");
|
|
152
|
+
} else if (url.pathname === "/slow") {
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
155
|
+
res.end("Slow response");
|
|
156
|
+
}, 100);
|
|
157
|
+
} else {
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end("Not Found");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await new Promise<void>((resolve) => {
|
|
164
|
+
server.listen(HTTP_PORT, () => resolve());
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
serversToCleanup.push(server);
|
|
168
|
+
return server;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Start an HTTPS server with given certificates
|
|
173
|
+
*/
|
|
174
|
+
async function startHttpsServer(
|
|
175
|
+
certPath: string,
|
|
176
|
+
keyPath: string,
|
|
177
|
+
): Promise<https.Server> {
|
|
178
|
+
const server = https.createServer(
|
|
179
|
+
{
|
|
180
|
+
cert: fs.readFileSync(certPath),
|
|
181
|
+
key: fs.readFileSync(keyPath),
|
|
182
|
+
},
|
|
183
|
+
(req, res) => {
|
|
184
|
+
const url = new URL(req.url!, `https://localhost:${HTTPS_PORT}`);
|
|
185
|
+
|
|
186
|
+
if (url.pathname === "/secure") {
|
|
187
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
188
|
+
res.end(
|
|
189
|
+
JSON.stringify({ message: "Hello from HTTPS server", secure: true }),
|
|
190
|
+
);
|
|
191
|
+
} else if (url.pathname === "/headers") {
|
|
192
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
193
|
+
res.end(JSON.stringify({ headers: req.headers }));
|
|
194
|
+
} else {
|
|
195
|
+
res.writeHead(404);
|
|
196
|
+
res.end("Not Found");
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await new Promise<void>((resolve) => {
|
|
202
|
+
server.listen(HTTPS_PORT, () => resolve());
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
serversToCleanup.push(server);
|
|
206
|
+
return server;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
describe("fetchwithRequestOptions E2E tests", () => {
|
|
210
|
+
describe("HTTP requests", () => {
|
|
211
|
+
test("should make basic HTTP GET request", async () => {
|
|
212
|
+
await startHttpServer();
|
|
213
|
+
|
|
214
|
+
const response = await fetchwithRequestOptions(
|
|
215
|
+
`http://localhost:${HTTP_PORT}/json`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(response.status).toBe(200);
|
|
219
|
+
const data = (await response.json()) as {
|
|
220
|
+
message: string;
|
|
221
|
+
method: string;
|
|
222
|
+
};
|
|
223
|
+
expect(data).toEqual({
|
|
224
|
+
message: "Hello from HTTP server",
|
|
225
|
+
method: "GET",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("should make HTTP POST request with body", async () => {
|
|
230
|
+
await startHttpServer();
|
|
231
|
+
|
|
232
|
+
const response = await fetchwithRequestOptions(
|
|
233
|
+
`http://localhost:${HTTP_PORT}/json`,
|
|
234
|
+
{
|
|
235
|
+
method: "POST",
|
|
236
|
+
body: JSON.stringify({ test: "data" }),
|
|
237
|
+
headers: { "Content-Type": "application/json" },
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(response.status).toBe(200);
|
|
242
|
+
const data = (await response.json()) as {
|
|
243
|
+
message: string;
|
|
244
|
+
method: string;
|
|
245
|
+
};
|
|
246
|
+
expect(data.method).toBe("POST");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("should handle custom headers", async () => {
|
|
250
|
+
await startHttpServer();
|
|
251
|
+
|
|
252
|
+
const response = await fetchwithRequestOptions(
|
|
253
|
+
`http://localhost:${HTTP_PORT}/headers`,
|
|
254
|
+
{
|
|
255
|
+
headers: { "X-Custom-Header": "test-value" },
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const data = (await response.json()) as {
|
|
260
|
+
headers: Record<string, string>;
|
|
261
|
+
};
|
|
262
|
+
expect(data.headers["x-custom-header"]).toBe("test-value");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("should handle error responses", async () => {
|
|
266
|
+
await startHttpServer();
|
|
267
|
+
|
|
268
|
+
const response = await fetchwithRequestOptions(
|
|
269
|
+
`http://localhost:${HTTP_PORT}/status/404`,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(response.status).toBe(404);
|
|
273
|
+
expect(response.ok).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("Request options integration", () => {
|
|
278
|
+
test("should merge extra body properties", async () => {
|
|
279
|
+
await startHttpServer();
|
|
280
|
+
|
|
281
|
+
const response = await fetchwithRequestOptions(
|
|
282
|
+
`http://localhost:${HTTP_PORT}/body`,
|
|
283
|
+
{
|
|
284
|
+
method: "POST",
|
|
285
|
+
body: JSON.stringify({ original: "data" }),
|
|
286
|
+
headers: { "Content-Type": "application/json" },
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
extraBodyProperties: { extra: "property" },
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(response.status).toBe(200);
|
|
294
|
+
const data = (await response.json()) as {
|
|
295
|
+
body: string;
|
|
296
|
+
headers: Record<string, string>;
|
|
297
|
+
};
|
|
298
|
+
const body = JSON.parse(data.body);
|
|
299
|
+
expect(body).toEqual({ original: "data", extra: "property" });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("should handle RequestOptions headers", async () => {
|
|
303
|
+
await startHttpServer();
|
|
304
|
+
|
|
305
|
+
const response = await fetchwithRequestOptions(
|
|
306
|
+
`http://localhost:${HTTP_PORT}/headers`,
|
|
307
|
+
{},
|
|
308
|
+
{
|
|
309
|
+
headers: { "X-Request-Options-Header": "from-request-options" },
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const data = (await response.json()) as {
|
|
314
|
+
headers: Record<string, string>;
|
|
315
|
+
};
|
|
316
|
+
expect(data.headers["x-request-options-header"]).toBe(
|
|
317
|
+
"from-request-options",
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("should merge headers from init and requestOptions", async () => {
|
|
322
|
+
await startHttpServer();
|
|
323
|
+
|
|
324
|
+
const response = await fetchwithRequestOptions(
|
|
325
|
+
`http://localhost:${HTTP_PORT}/headers`,
|
|
326
|
+
{
|
|
327
|
+
headers: { "X-Init-Header": "from-init" },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
headers: { "X-Request-Options-Header": "from-request-options" },
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const data = (await response.json()) as {
|
|
335
|
+
headers: Record<string, string>;
|
|
336
|
+
};
|
|
337
|
+
expect(data.headers["x-init-header"]).toBe("from-init");
|
|
338
|
+
expect(data.headers["x-request-options-header"]).toBe(
|
|
339
|
+
"from-request-options",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("Certificate handling - Enterprise scenarios", () => {
|
|
345
|
+
test("should REJECT self-signed certificates when SSL verification is enabled", async () => {
|
|
346
|
+
const tempDir = createTempDir();
|
|
347
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
348
|
+
cn: "localhost",
|
|
349
|
+
san: ["localhost", "127.0.0.1"],
|
|
350
|
+
org: "Test",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
354
|
+
|
|
355
|
+
// This simulates the customer's "unable to get local issuer certificate" error
|
|
356
|
+
await expect(
|
|
357
|
+
fetchwithRequestOptions(
|
|
358
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
359
|
+
{},
|
|
360
|
+
{ verifySsl: true }, // No custom CA provided
|
|
361
|
+
),
|
|
362
|
+
).rejects.toThrow(); // Should fail with certificate error
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("should ACCEPT self-signed certificates when SSL verification is disabled", async () => {
|
|
366
|
+
const tempDir = createTempDir();
|
|
367
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
368
|
+
cn: "localhost",
|
|
369
|
+
san: ["localhost", "127.0.0.1"],
|
|
370
|
+
org: "Test",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
374
|
+
|
|
375
|
+
const response = await fetchwithRequestOptions(
|
|
376
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
377
|
+
{},
|
|
378
|
+
{ verifySsl: false },
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(response.status).toBe(200);
|
|
382
|
+
const data = (await response.json()) as {
|
|
383
|
+
message: string;
|
|
384
|
+
secure: boolean;
|
|
385
|
+
};
|
|
386
|
+
expect(data.secure).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("should ACCEPT self-signed certificates with custom CA bundle", async () => {
|
|
390
|
+
const tempDir = createTempDir();
|
|
391
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
392
|
+
cn: "localhost",
|
|
393
|
+
san: ["localhost", "127.0.0.1"],
|
|
394
|
+
org: "Test",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
398
|
+
|
|
399
|
+
const response = await fetchwithRequestOptions(
|
|
400
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
401
|
+
{},
|
|
402
|
+
{
|
|
403
|
+
caBundlePath: serverCerts.caCertPath, // Our self-signed cert as CA
|
|
404
|
+
verifySsl: true,
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(response.status).toBe(200);
|
|
409
|
+
const data = (await response.json()) as {
|
|
410
|
+
message: string;
|
|
411
|
+
secure: boolean;
|
|
412
|
+
};
|
|
413
|
+
expect(data.secure).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("should REJECT certificates when provided WRONG CA bundle", async () => {
|
|
417
|
+
const tempDir = createTempDir();
|
|
418
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
419
|
+
cn: "localhost",
|
|
420
|
+
san: ["localhost", "127.0.0.1"],
|
|
421
|
+
org: "Test",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Generate a different certificate as the "wrong" CA
|
|
425
|
+
const wrongCaCerts = await generateCertificate(tempDir, "wrong-ca", {
|
|
426
|
+
cn: "wrong-ca",
|
|
427
|
+
san: ["wrong-ca"],
|
|
428
|
+
org: "Wrong CA",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
432
|
+
|
|
433
|
+
// This should fail because the wrong CA can't validate our server certificate
|
|
434
|
+
await expect(
|
|
435
|
+
fetchwithRequestOptions(
|
|
436
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
437
|
+
{},
|
|
438
|
+
{
|
|
439
|
+
caBundlePath: wrongCaCerts.caCertPath,
|
|
440
|
+
verifySsl: true,
|
|
441
|
+
},
|
|
442
|
+
),
|
|
443
|
+
).rejects.toThrow(); // Should fail with certificate validation error
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("should REJECT certificates when CA bundle file is corrupted", async () => {
|
|
447
|
+
const tempDir = createTempDir();
|
|
448
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
449
|
+
cn: "localhost",
|
|
450
|
+
san: ["localhost", "127.0.0.1"],
|
|
451
|
+
org: "Test",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Create a corrupted/invalid certificate file
|
|
455
|
+
const corruptedCaPath = path.join(tempDir, "corrupted-ca.pem");
|
|
456
|
+
const corruptedContent = `-----BEGIN CERTIFICATE-----
|
|
457
|
+
This is not a valid certificate content
|
|
458
|
+
Just some random text that looks like a cert
|
|
459
|
+
-----END CERTIFICATE-----`;
|
|
460
|
+
|
|
461
|
+
fs.writeFileSync(corruptedCaPath, corruptedContent);
|
|
462
|
+
|
|
463
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
464
|
+
|
|
465
|
+
// This should fail because the corrupted CA file can't be parsed
|
|
466
|
+
await expect(
|
|
467
|
+
fetchwithRequestOptions(
|
|
468
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
469
|
+
{},
|
|
470
|
+
{
|
|
471
|
+
caBundlePath: corruptedCaPath,
|
|
472
|
+
verifySsl: true,
|
|
473
|
+
},
|
|
474
|
+
),
|
|
475
|
+
).rejects.toThrow(); // Should fail with certificate parsing or validation error
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("should REJECT certificates when CA bundle file does not exist", async () => {
|
|
479
|
+
const tempDir = createTempDir();
|
|
480
|
+
const serverCerts = await generateCertificate(tempDir, "server", {
|
|
481
|
+
cn: "localhost",
|
|
482
|
+
san: ["localhost", "127.0.0.1"],
|
|
483
|
+
org: "Test",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const nonExistentCaPath = path.join(tempDir, "does-not-exist.pem");
|
|
487
|
+
|
|
488
|
+
await startHttpsServer(serverCerts.certPath, serverCerts.keyPath);
|
|
489
|
+
|
|
490
|
+
// This should handle the missing file gracefully but still fail cert validation
|
|
491
|
+
await expect(
|
|
492
|
+
fetchwithRequestOptions(
|
|
493
|
+
`https://localhost:${HTTPS_PORT}/secure`,
|
|
494
|
+
{},
|
|
495
|
+
{
|
|
496
|
+
caBundlePath: nonExistentCaPath,
|
|
497
|
+
verifySsl: true,
|
|
498
|
+
},
|
|
499
|
+
),
|
|
500
|
+
).rejects.toThrow(); // Should fail with certificate validation error
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("Error handling", () => {
|
|
505
|
+
test("should handle network errors gracefully", async () => {
|
|
506
|
+
// Try to connect to a non-existent server
|
|
507
|
+
await expect(
|
|
508
|
+
fetchwithRequestOptions("http://localhost:9999/nonexistent"),
|
|
509
|
+
).rejects.toThrow();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("should handle malformed URLs", async () => {
|
|
513
|
+
await expect(
|
|
514
|
+
fetchwithRequestOptions("not-a-valid-url"),
|
|
515
|
+
).rejects.toThrow();
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
});
|
package/src/fetch.ts
CHANGED
|
@@ -107,9 +107,30 @@ export async function fetchwithRequestOptions(
|
|
|
107
107
|
: new protocol.Agent(agentOptions);
|
|
108
108
|
|
|
109
109
|
let headers: { [key: string]: string } = {};
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
|
|
111
|
+
// Handle different header formats
|
|
112
|
+
if (init?.headers) {
|
|
113
|
+
const headersSource = init.headers as any;
|
|
114
|
+
|
|
115
|
+
// Check if it's a Headers-like object (OpenAI v5 HeadersList, standard Headers)
|
|
116
|
+
if (headersSource && typeof headersSource.forEach === "function") {
|
|
117
|
+
// Use forEach method which works reliably on Headers objects
|
|
118
|
+
headersSource.forEach((value: string, key: string) => {
|
|
119
|
+
headers[key] = value;
|
|
120
|
+
});
|
|
121
|
+
} else if (Array.isArray(headersSource)) {
|
|
122
|
+
// This is an array of [key, value] tuples
|
|
123
|
+
for (const [key, value] of headersSource) {
|
|
124
|
+
headers[key] = value as string;
|
|
125
|
+
}
|
|
126
|
+
} else if (headersSource && typeof headersSource === "object") {
|
|
127
|
+
// This is a plain object
|
|
128
|
+
for (const [key, value] of Object.entries(headersSource)) {
|
|
129
|
+
headers[key] = value as string;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
112
132
|
}
|
|
133
|
+
|
|
113
134
|
headers = {
|
|
114
135
|
...headers,
|
|
115
136
|
...requestOptions?.headers,
|