sliftutils 1.7.17 → 1.7.18
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/.claude/settings.local.json +2 -1
- package/dist/test.ts.cache +43 -8
- package/misc/https/hostServer.ts +5 -1
- package/package.json +2 -2
- package/storage/remoteFileServer.ts +1 -8
- package/test.ts +45 -6
- package/yarn.lock +4 -4
package/dist/test.ts.cache
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
-
const
|
|
6
|
+
const net_1 = __importDefault(require("net"));
|
|
7
|
+
const forwardPort_1 = require("socket-function/src/forwardPort");
|
|
8
|
+
const TEST_PORT = 5299;
|
|
9
|
+
const EXTERNAL_IP = "99.250.124.91";
|
|
10
|
+
function testConnect(host, port) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
let socket = net_1.default.connect({ host, port, timeout: 8000 });
|
|
13
|
+
socket.on("connect", () => {
|
|
14
|
+
socket.destroy();
|
|
15
|
+
resolve("CONNECTED");
|
|
16
|
+
});
|
|
17
|
+
socket.on("timeout", () => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
resolve("TIMEOUT");
|
|
20
|
+
});
|
|
21
|
+
socket.on("error", e => resolve(e.code || String(e)));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
4
24
|
async function main() {
|
|
5
|
-
let
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
let lanIP = await (0, forwardPort_1.getLocalInternalIP)();
|
|
26
|
+
console.log("our LAN IP:", lanIP);
|
|
27
|
+
let listener = net_1.default.createServer(socket => {
|
|
28
|
+
socket.on("error", () => socket.destroy());
|
|
29
|
+
socket.end("hello");
|
|
8
30
|
});
|
|
9
|
-
|
|
31
|
+
await new Promise(resolve => listener.listen(TEST_PORT, "0.0.0.0", () => resolve()));
|
|
32
|
+
console.log(`listening on 0.0.0.0:${TEST_PORT}`);
|
|
33
|
+
await (0, forwardPort_1.forwardPort)({ externalPort: TEST_PORT, internalPort: TEST_PORT });
|
|
34
|
+
let mappings = await (0, forwardPort_1.listPortMappings)();
|
|
35
|
+
let ours = mappings.find(x => x.externalPort === TEST_PORT);
|
|
36
|
+
console.log(`mapping for ${TEST_PORT} after create:`, JSON.stringify(ours) || "MISSING");
|
|
37
|
+
console.log(`LAN direct (${lanIP}:${TEST_PORT}):`, await testConnect(lanIP || "127.0.0.1", TEST_PORT));
|
|
38
|
+
console.log(`hairpin (${EXTERNAL_IP}:${TEST_PORT}):`, await testConnect(EXTERNAL_IP, TEST_PORT));
|
|
39
|
+
listener.close();
|
|
40
|
+
process.exit(0);
|
|
10
41
|
}
|
|
11
|
-
main().catch(
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
main().catch(e => {
|
|
43
|
+
var _a;
|
|
44
|
+
console.error((_a = e.stack) !== null && _a !== void 0 ? _a : e);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSw4Q0FBc0I7QUFDdEIsaUVBQW9HO0FBRXBHLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQztBQUN2QixNQUFNLFdBQVcsR0FBRyxlQUFlLENBQUM7QUFFcEMsU0FBUyxXQUFXLENBQUMsSUFBWSxFQUFFLElBQVk7SUFDM0MsT0FBTyxJQUFJLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBRTtRQUN6QixJQUFJLE1BQU0sR0FBRyxhQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUN4RCxNQUFNLENBQUMsRUFBRSxDQUFDLFNBQVMsRUFBRSxHQUFHLEVBQUU7WUFDdEIsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ2pCLE9BQU8sQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUN6QixDQUFDLENBQUMsQ0FBQztRQUNILE1BQU0sQ0FBQyxFQUFFLENBQUMsU0FBUyxFQUFFLEdBQUcsRUFBRTtZQUN0QixNQUFNLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDakIsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ3ZCLENBQUMsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUUsQ0FBdUIsQ0FBQyxJQUFJLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNqRixDQUFDLENBQUMsQ0FBQztBQUNQLENBQUM7QUFFRCxLQUFLLFVBQVUsSUFBSTtJQUNmLElBQUksS0FBSyxHQUFHLE1BQU0sSUFBQSxnQ0FBa0IsR0FBRSxDQUFDO0lBQ3ZDLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBRWxDLElBQUksUUFBUSxHQUFHLGFBQUcsQ0FBQyxZQUFZLENBQUMsTUFBTSxDQUFDLEVBQUU7UUFDckMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDM0MsTUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN4QixDQUFDLENBQUMsQ0FBQztJQUNILE1BQU0sSUFBSSxPQUFPLENBQU8sT0FBTyxDQUFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQzNGLE9BQU8sQ0FBQyxHQUFHLENBQUMsd0JBQXdCLFNBQVMsRUFBRSxDQUFDLENBQUM7SUFFakQsTUFBTSxJQUFBLHlCQUFXLEVBQUMsRUFBRSxZQUFZLEVBQUUsU0FBUyxFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDO0lBRXhFLElBQUksUUFBUSxHQUFHLE1BQU0sSUFBQSw4QkFBZ0IsR0FBRSxDQUFDO0lBQ3hDLElBQUksSUFBSSxHQUFHLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsWUFBWSxLQUFLLFNBQVMsQ0FBQyxDQUFDO0lBQzVELE9BQU8sQ0FBQyxHQUFHLENBQUMsZUFBZSxTQUFTLGdCQUFnQixFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLElBQUksU0FBUyxDQUFDLENBQUM7SUFFekYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxlQUFlLEtBQUssSUFBSSxTQUFTLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQyxLQUFLLElBQUksV0FBVyxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUM7SUFDdkcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxZQUFZLFdBQVcsSUFBSSxTQUFTLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQyxXQUFXLEVBQUUsU0FBUyxDQUFDLENBQUMsQ0FBQztJQUVqRyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDakIsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNwQixDQUFDO0FBRUQsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFOztJQUNiLE9BQU8sQ0FBQyxLQUFLLENBQUMsTUFBQyxDQUFXLENBQUMsS0FBSyxtQ0FBSSxDQUFDLENBQUMsQ0FBQztJQUN2QyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3BCLENBQUMsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IG5ldCBmcm9tIFwibmV0XCI7XG5pbXBvcnQgeyBsaXN0UG9ydE1hcHBpbmdzLCBmb3J3YXJkUG9ydCwgZ2V0TG9jYWxJbnRlcm5hbElQIH0gZnJvbSBcInNvY2tldC1mdW5jdGlvbi9zcmMvZm9yd2FyZFBvcnRcIjtcblxuY29uc3QgVEVTVF9QT1JUID0gNTI5OTtcbmNvbnN0IEVYVEVSTkFMX0lQID0gXCI5OS4yNTAuMTI0LjkxXCI7XG5cbmZ1bmN0aW9uIHRlc3RDb25uZWN0KGhvc3Q6IHN0cmluZywgcG9ydDogbnVtYmVyKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICByZXR1cm4gbmV3IFByb21pc2UocmVzb2x2ZSA9PiB7XG4gICAgICAgIGxldCBzb2NrZXQgPSBuZXQuY29ubmVjdCh7IGhvc3QsIHBvcnQsIHRpbWVvdXQ6IDgwMDAgfSk7XG4gICAgICAgIHNvY2tldC5vbihcImNvbm5lY3RcIiwgKCkgPT4ge1xuICAgICAgICAgICAgc29ja2V0LmRlc3Ryb3koKTtcbiAgICAgICAgICAgIHJlc29sdmUoXCJDT05ORUNURURcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBzb2NrZXQub24oXCJ0aW1lb3V0XCIsICgpID0+IHtcbiAgICAgICAgICAgIHNvY2tldC5kZXN0cm95KCk7XG4gICAgICAgICAgICByZXNvbHZlKFwiVElNRU9VVFwiKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHNvY2tldC5vbihcImVycm9yXCIsIGUgPT4gcmVzb2x2ZSgoZSBhcyB7IGNvZGU/OiBzdHJpbmcgfSkuY29kZSB8fCBTdHJpbmcoZSkpKTtcbiAgICB9KTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gbWFpbigpIHtcbiAgICBsZXQgbGFuSVAgPSBhd2FpdCBnZXRMb2NhbEludGVybmFsSVAoKTtcbiAgICBjb25zb2xlLmxvZyhcIm91ciBMQU4gSVA6XCIsIGxhbklQKTtcblxuICAgIGxldCBsaXN0ZW5lciA9IG5ldC5jcmVhdGVTZXJ2ZXIoc29ja2V0ID0+IHtcbiAgICAgICAgc29ja2V0Lm9uKFwiZXJyb3JcIiwgKCkgPT4gc29ja2V0LmRlc3Ryb3koKSk7XG4gICAgICAgIHNvY2tldC5lbmQoXCJoZWxsb1wiKTtcbiAgICB9KTtcbiAgICBhd2FpdCBuZXcgUHJvbWlzZTx2b2lkPihyZXNvbHZlID0+IGxpc3RlbmVyLmxpc3RlbihURVNUX1BPUlQsIFwiMC4wLjAuMFwiLCAoKSA9PiByZXNvbHZlKCkpKTtcbiAgICBjb25zb2xlLmxvZyhgbGlzdGVuaW5nIG9uIDAuMC4wLjA6JHtURVNUX1BPUlR9YCk7XG5cbiAgICBhd2FpdCBmb3J3YXJkUG9ydCh7IGV4dGVybmFsUG9ydDogVEVTVF9QT1JULCBpbnRlcm5hbFBvcnQ6IFRFU1RfUE9SVCB9KTtcblxuICAgIGxldCBtYXBwaW5ncyA9IGF3YWl0IGxpc3RQb3J0TWFwcGluZ3MoKTtcbiAgICBsZXQgb3VycyA9IG1hcHBpbmdzLmZpbmQoeCA9PiB4LmV4dGVybmFsUG9ydCA9PT0gVEVTVF9QT1JUKTtcbiAgICBjb25zb2xlLmxvZyhgbWFwcGluZyBmb3IgJHtURVNUX1BPUlR9IGFmdGVyIGNyZWF0ZTpgLCBKU09OLnN0cmluZ2lmeShvdXJzKSB8fCBcIk1JU1NJTkdcIik7XG5cbiAgICBjb25zb2xlLmxvZyhgTEFOIGRpcmVjdCAoJHtsYW5JUH06JHtURVNUX1BPUlR9KTpgLCBhd2FpdCB0ZXN0Q29ubmVjdChsYW5JUCB8fCBcIjEyNy4wLjAuMVwiLCBURVNUX1BPUlQpKTtcbiAgICBjb25zb2xlLmxvZyhgaGFpcnBpbiAoJHtFWFRFUk5BTF9JUH06JHtURVNUX1BPUlR9KTpgLCBhd2FpdCB0ZXN0Q29ubmVjdChFWFRFUk5BTF9JUCwgVEVTVF9QT1JUKSk7XG5cbiAgICBsaXN0ZW5lci5jbG9zZSgpO1xuICAgIHByb2Nlc3MuZXhpdCgwKTtcbn1cblxubWFpbigpLmNhdGNoKGUgPT4ge1xuICAgIGNvbnNvbGUuZXJyb3IoKGUgYXMgRXJyb3IpLnN0YWNrID8/IGUpO1xuICAgIHByb2Nlc3MuZXhpdCgxKTtcbn0pO1xuIl1//9
|
|
48
|
+
/* _JS_SOURCE_HASH = "ff635e41519c7464cb3b055e6fe334893caa4fa66ebb080597c5fd80309af3da"; */
|
package/misc/https/hostServer.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getNodeIdLocation } from "socket-function/src/nodeCache";
|
|
|
6
6
|
import { timeInMinute } from "socket-function/src/misc";
|
|
7
7
|
import { delay } from "socket-function/src/batching";
|
|
8
8
|
import { getExternalIP } from "socket-function/src/networking";
|
|
9
|
+
import { forwardPort } from "socket-function/src/forwardPort";
|
|
9
10
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
10
11
|
import { getOwnMachineId, getThreadKeyCert, loadIdentityCA } from "./certs";
|
|
11
12
|
import { generateCert, getAccountKey, parseCert } from "./httpsCerts";
|
|
@@ -79,7 +80,7 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
|
79
80
|
cert: threadCert.cert,
|
|
80
81
|
});
|
|
81
82
|
},
|
|
82
|
-
["127-0-0-1." +
|
|
83
|
+
["127-0-0-1." + rootDomain]: async callback => {
|
|
83
84
|
callback(keyCert);
|
|
84
85
|
certListeners.push(callback);
|
|
85
86
|
},
|
|
@@ -120,6 +121,9 @@ async function runMainPortAcquireLoop(domain: string, mainPort: number, servingP
|
|
|
120
121
|
});
|
|
121
122
|
if (acquired) {
|
|
122
123
|
console.log(magenta(`Acquired main port ${mainPort} for https://${domain} (relaying to our listener on ${servingPort})`));
|
|
124
|
+
// The mount only forwarded OUR serving port; the main port needs its own mapping (the
|
|
125
|
+
// predecessor's mapping pointed at the same machine+port, but dies with it)
|
|
126
|
+
await forwardPort({ externalPort: mainPort, internalPort: mainPort });
|
|
123
127
|
fallback.onListening(mainPort, true);
|
|
124
128
|
return;
|
|
125
129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sliftutils",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.18",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
58
58
|
"preact-old-types": "^10.28.1",
|
|
59
59
|
"shell-quote": "^1.8.3",
|
|
60
|
-
"socket-function": "^1.2.
|
|
60
|
+
"socket-function": "^1.2.24",
|
|
61
61
|
"typenode": "^6.6.1",
|
|
62
62
|
"typesafecss": "*",
|
|
63
63
|
"ws": "^8.18.3",
|
|
@@ -671,14 +671,7 @@ export async function runFileHoster(): Promise<void> {
|
|
|
671
671
|
console.log(" self-signed certificate warning, then connect from the app.");
|
|
672
672
|
console.log("");
|
|
673
673
|
|
|
674
|
-
|
|
675
|
-
// Linux (forwardPort returns early there).
|
|
676
|
-
const refresh = async () => {
|
|
677
|
-
try { await forwardPort({ externalPort: info.port, internalPort: info.port }); }
|
|
678
|
-
catch (e) { console.warn(" port forwarding failed:", (e as Error).message); }
|
|
679
|
-
};
|
|
680
|
-
await refresh();
|
|
681
|
-
setInterval(refresh, 30 * 60 * 1000);
|
|
674
|
+
await forwardPort({ externalPort: info.port, internalPort: info.port });
|
|
682
675
|
|
|
683
676
|
console.log(" [access] request logging on (batched every 5s)\n");
|
|
684
677
|
|
package/test.ts
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
|
-
import
|
|
1
|
+
import net from "net";
|
|
2
|
+
import { listPortMappings, forwardPort, getLocalInternalIP } from "socket-function/src/forwardPort";
|
|
3
|
+
|
|
4
|
+
const TEST_PORT = 5299;
|
|
5
|
+
const EXTERNAL_IP = "99.250.124.91";
|
|
6
|
+
|
|
7
|
+
function testConnect(host: string, port: number): Promise<string> {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
let socket = net.connect({ host, port, timeout: 8000 });
|
|
10
|
+
socket.on("connect", () => {
|
|
11
|
+
socket.destroy();
|
|
12
|
+
resolve("CONNECTED");
|
|
13
|
+
});
|
|
14
|
+
socket.on("timeout", () => {
|
|
15
|
+
socket.destroy();
|
|
16
|
+
resolve("TIMEOUT");
|
|
17
|
+
});
|
|
18
|
+
socket.on("error", e => resolve((e as { code?: string }).code || String(e)));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
async function main() {
|
|
4
|
-
let
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
let lanIP = await getLocalInternalIP();
|
|
24
|
+
console.log("our LAN IP:", lanIP);
|
|
25
|
+
|
|
26
|
+
let listener = net.createServer(socket => {
|
|
27
|
+
socket.on("error", () => socket.destroy());
|
|
28
|
+
socket.end("hello");
|
|
7
29
|
});
|
|
8
|
-
|
|
30
|
+
await new Promise<void>(resolve => listener.listen(TEST_PORT, "0.0.0.0", () => resolve()));
|
|
31
|
+
console.log(`listening on 0.0.0.0:${TEST_PORT}`);
|
|
32
|
+
|
|
33
|
+
await forwardPort({ externalPort: TEST_PORT, internalPort: TEST_PORT });
|
|
34
|
+
|
|
35
|
+
let mappings = await listPortMappings();
|
|
36
|
+
let ours = mappings.find(x => x.externalPort === TEST_PORT);
|
|
37
|
+
console.log(`mapping for ${TEST_PORT} after create:`, JSON.stringify(ours) || "MISSING");
|
|
38
|
+
|
|
39
|
+
console.log(`LAN direct (${lanIP}:${TEST_PORT}):`, await testConnect(lanIP || "127.0.0.1", TEST_PORT));
|
|
40
|
+
console.log(`hairpin (${EXTERNAL_IP}:${TEST_PORT}):`, await testConnect(EXTERNAL_IP, TEST_PORT));
|
|
41
|
+
|
|
42
|
+
listener.close();
|
|
43
|
+
process.exit(0);
|
|
9
44
|
}
|
|
10
|
-
|
|
45
|
+
|
|
46
|
+
main().catch(e => {
|
|
47
|
+
console.error((e as Error).stack ?? e);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
package/yarn.lock
CHANGED
|
@@ -1944,10 +1944,10 @@ slash@^3.0.0:
|
|
|
1944
1944
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
|
1945
1945
|
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
|
1946
1946
|
|
|
1947
|
-
socket-function@^1.2.
|
|
1948
|
-
version "1.2.
|
|
1949
|
-
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.
|
|
1950
|
-
integrity sha512-
|
|
1947
|
+
socket-function@^1.2.24:
|
|
1948
|
+
version "1.2.24"
|
|
1949
|
+
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.24.tgz#3683e93f0fe84a8b3076fd9cd18b45baef941286"
|
|
1950
|
+
integrity sha512-Y6da2A16MUPc2E/yJ4tJJAWXszIxgYaA/sVz0gKdZCb5nADCZeUAXE15bfkhX9HWn0zQv8AzXXkKHs78vhQVwQ==
|
|
1951
1951
|
dependencies:
|
|
1952
1952
|
"@types/pako" "^2.0.3"
|
|
1953
1953
|
"@types/ws" "^8.5.3"
|