inertjs-conduit 1.0.0-beta.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/package.json +9 -0
- package/src/index.js +62 -0
- package/src/worker.js +31 -0
- package/test/conduit.test.js +32 -0
package/package.json
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
let worker;
|
|
9
|
+
let idCounter = 0;
|
|
10
|
+
const pending = new Map();
|
|
11
|
+
|
|
12
|
+
function getWorker() {
|
|
13
|
+
if (!worker) {
|
|
14
|
+
worker = new Worker(path.join(__dirname, 'worker.js'));
|
|
15
|
+
worker.on('message', (msg) => {
|
|
16
|
+
const p = pending.get(msg.id);
|
|
17
|
+
if (!p) return;
|
|
18
|
+
pending.delete(msg.id);
|
|
19
|
+
|
|
20
|
+
if (msg.error) {
|
|
21
|
+
p.reject(new Error(msg.error));
|
|
22
|
+
} else {
|
|
23
|
+
const textData = Buffer.from(msg.data).toString('utf8');
|
|
24
|
+
p.resolve({
|
|
25
|
+
status: msg.status,
|
|
26
|
+
headers: msg.headers,
|
|
27
|
+
data: msg.data,
|
|
28
|
+
text: () => Promise.resolve(textData),
|
|
29
|
+
json: () => Promise.resolve(JSON.parse(textData))
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
worker.on('error', (err) => {
|
|
34
|
+
for (const p of pending.values()) p.reject(err);
|
|
35
|
+
pending.clear();
|
|
36
|
+
worker = null;
|
|
37
|
+
});
|
|
38
|
+
worker.on('exit', () => {
|
|
39
|
+
for (const p of pending.values()) p.reject(new Error('Conduit worker exited unexpectedly'));
|
|
40
|
+
pending.clear();
|
|
41
|
+
worker = null;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return worker;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Securely fetch data in a background thread, preventing Event Loop blocking.
|
|
49
|
+
* Sensitive headers (authorization, cookies) are automatically stripped from the response.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @param {RequestInit} options
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchSecure(url, options = {}) {
|
|
55
|
+
const w = getWorker();
|
|
56
|
+
const id = ++idCounter;
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
pending.set(id, { resolve, reject });
|
|
60
|
+
w.postMessage({ id, url, options });
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie'];
|
|
4
|
+
|
|
5
|
+
parentPort.on('message', async ({ id, url, options }) => {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(url, options);
|
|
8
|
+
const headers = {};
|
|
9
|
+
for (const [key, value] of res.headers.entries()) {
|
|
10
|
+
if (!FORBIDDEN_HEADERS.includes(key.toLowerCase())) {
|
|
11
|
+
headers[key] = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
16
|
+
// Use Uint8Array which is efficiently clonable to the main thread
|
|
17
|
+
const data = new Uint8Array(arrayBuffer);
|
|
18
|
+
|
|
19
|
+
parentPort.postMessage({
|
|
20
|
+
id,
|
|
21
|
+
status: res.status,
|
|
22
|
+
headers,
|
|
23
|
+
data
|
|
24
|
+
});
|
|
25
|
+
} catch (err) {
|
|
26
|
+
parentPort.postMessage({
|
|
27
|
+
id,
|
|
28
|
+
error: err.message
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { fetchSecure } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
test('Conduit secure fetch subsystem', async (t) => {
|
|
7
|
+
// Create a local HTTP server to test against
|
|
8
|
+
const server = http.createServer((req, res) => {
|
|
9
|
+
res.setHeader('Set-Cookie', 'secret=123');
|
|
10
|
+
res.setHeader('Authorization', 'Bearer token');
|
|
11
|
+
res.setHeader('Content-Type', 'application/json');
|
|
12
|
+
res.setHeader('X-Safe-Header', 'hello');
|
|
13
|
+
res.end(JSON.stringify({ message: 'Hello from Conduit' }));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await new Promise(resolve => server.listen(0, resolve));
|
|
17
|
+
const port = server.address().port;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetchSecure(`http://127.0.0.1:${port}/test`);
|
|
21
|
+
|
|
22
|
+
assert.strictEqual(res.status, 200);
|
|
23
|
+
assert.strictEqual(res.headers['x-safe-header'], 'hello');
|
|
24
|
+
assert.strictEqual(res.headers['set-cookie'], undefined, 'Cookie header should be stripped');
|
|
25
|
+
assert.strictEqual(res.headers['authorization'], undefined, 'Auth header should be stripped');
|
|
26
|
+
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
assert.deepStrictEqual(data, { message: 'Hello from Conduit' });
|
|
29
|
+
} finally {
|
|
30
|
+
server.close();
|
|
31
|
+
}
|
|
32
|
+
});
|