@decentnetwork/lan 0.1.194 → 0.1.196
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/cli/commands.js +49 -3
- package/dist/proxy/hls-prefetch.d.ts +5 -0
- package/dist/proxy/hls-prefetch.js +21 -0
- package/dist/proxy/multi-exit-router.js +48 -19
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* CLI command handlers
|
|
3
3
|
*/
|
|
4
4
|
import { resolve, dirname, join } from "path";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, copyFileSync } from "fs";
|
|
6
6
|
import { createConnection } from "net";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
@@ -1421,6 +1421,39 @@ export async function cmdProxyTrustCa(args) {
|
|
|
1421
1421
|
return;
|
|
1422
1422
|
}
|
|
1423
1423
|
if (process.platform === "linux") {
|
|
1424
|
+
// WSL: the browser runs on the WINDOWS side and trusts the WINDOWS cert
|
|
1425
|
+
// store — installing into this distro's Linux store does nothing for it.
|
|
1426
|
+
// certutil -user -addstore ROOT is rejected on modern Windows
|
|
1427
|
+
// (0x80070032 ERROR_NOT_SUPPORTED); PowerShell Import-Certificate into
|
|
1428
|
+
// CurrentUser\Root works without admin (Windows pops a confirmation dialog).
|
|
1429
|
+
const isWsl = existsSync("/proc/version") &&
|
|
1430
|
+
readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
|
|
1431
|
+
if (isWsl) {
|
|
1432
|
+
const winCopy = "/mnt/c/Users/Public/agentnet-mitm-ca.pem";
|
|
1433
|
+
const winPath = "C:\\Users\\Public\\agentnet-mitm-ca.pem";
|
|
1434
|
+
console.log("WSL detected — the browser lives on Windows, installing into the Windows user trust store.");
|
|
1435
|
+
try {
|
|
1436
|
+
copyFileSync(caCertPath, winCopy);
|
|
1437
|
+
console.log("A Windows security dialog will pop up — click Yes to trust the CA...");
|
|
1438
|
+
const w = spawnSync("powershell.exe", ["-NoProfile", "-Command", `Import-Certificate -FilePath '${winPath}' -CertStoreLocation Cert:\\CurrentUser\\Root`], { stdio: "inherit" });
|
|
1439
|
+
if (w.status === 0) {
|
|
1440
|
+
console.log("✓ Windows store updated. Chrome/Edge on Windows pick it up immediately (reload the page).");
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
console.log("Install failed or dialog declined — in a Windows PowerShell run:");
|
|
1444
|
+
console.log(` Import-Certificate -FilePath "${winPath}" -CertStoreLocation Cert:\\CurrentUser\\Root`);
|
|
1445
|
+
console.log(`(or, in an ADMIN terminal: certutil -addstore -f ROOT "${winPath}")`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
catch (e) {
|
|
1449
|
+
console.log(`Could not copy the cert to the Windows side (${e.message}). Manually:`);
|
|
1450
|
+
console.log(` cp "${caCertPath}" ${winCopy}`);
|
|
1451
|
+
console.log(` then in Windows PowerShell: Import-Certificate -FilePath "${winPath}" -CertStoreLocation Cert:\\CurrentUser\\Root`);
|
|
1452
|
+
}
|
|
1453
|
+
console.log("(Linux-side clients like curl inside WSL would additionally need the system CA store:");
|
|
1454
|
+
console.log(` sudo cp "${caCertPath}" /usr/local/share/ca-certificates/agentnet-mitm-ca.crt && sudo update-ca-certificates)`);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1424
1457
|
console.log("Installing into the system CA store (asks for your sudo password)...");
|
|
1425
1458
|
const cp = spawnSync("sudo", ["cp", caCertPath, "/usr/local/share/ca-certificates/agentnet-mitm-ca.crt"], { stdio: "inherit" });
|
|
1426
1459
|
const up = cp.status === 0 ? spawnSync("sudo", ["update-ca-certificates"], { stdio: "inherit" }) : cp;
|
|
@@ -1431,8 +1464,21 @@ export async function cmdProxyTrustCa(args) {
|
|
|
1431
1464
|
console.log("(package: libnss3-tools). Firefox: Settings → Certificates → Import.");
|
|
1432
1465
|
return;
|
|
1433
1466
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1467
|
+
if (process.platform === "win32") {
|
|
1468
|
+
console.log("Installing into the Windows user trust store (a security dialog will pop up — click Yes)...");
|
|
1469
|
+
const w = spawnSync("powershell.exe", ["-NoProfile", "-Command", `Import-Certificate -FilePath '${caCertPath}' -CertStoreLocation Cert:\\CurrentUser\\Root`], { stdio: "inherit" });
|
|
1470
|
+
if (w.status === 0) {
|
|
1471
|
+
console.log("✓ Trusted. Chrome/Edge pick it up immediately (reload the page).");
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
console.log("Install failed or dialog declined — in PowerShell run:");
|
|
1475
|
+
console.log(` Import-Certificate -FilePath "${caCertPath}" -CertStoreLocation Cert:\\CurrentUser\\Root`);
|
|
1476
|
+
console.log(`(or, in an ADMIN terminal: certutil -addstore -f ROOT "${caCertPath}")`);
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
console.log(`Unsupported platform ${process.platform} — import this CA into your browser's trust store manually:`);
|
|
1481
|
+
console.log(` ${caCertPath}`);
|
|
1436
1482
|
}
|
|
1437
1483
|
export async function cmdProxyRouter(args) {
|
|
1438
1484
|
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
@@ -24,6 +24,11 @@ export declare function parseM3u8Variants(playlist: string, baseUrl: string): Hl
|
|
|
24
24
|
* metadata and its following URI are removed as one pair. Media playlists are
|
|
25
25
|
* returned unchanged. */
|
|
26
26
|
export declare function capM3u8Variants(playlist: string, maxBandwidth: number): string;
|
|
27
|
+
/** Map variants above the delivery cap to the fastest variant that remains.
|
|
28
|
+
* Some players keep polling a previously selected media-playlist URL even after
|
|
29
|
+
* reloading the capped master playlist. The proxy can use this map to fetch the
|
|
30
|
+
* safe media playlist while preserving the browser-facing request URL. */
|
|
31
|
+
export declare function buildM3u8VariantFallbacks(playlist: string, baseUrl: string, maxBandwidth: number): Map<string, string>;
|
|
27
32
|
/**
|
|
28
33
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
29
34
|
* playlist's own URL. Returns [] for master playlists (variant lists) — those
|
|
@@ -107,6 +107,27 @@ export function capM3u8Variants(playlist, maxBandwidth) {
|
|
|
107
107
|
}
|
|
108
108
|
return out.join("\n");
|
|
109
109
|
}
|
|
110
|
+
/** Map variants above the delivery cap to the fastest variant that remains.
|
|
111
|
+
* Some players keep polling a previously selected media-playlist URL even after
|
|
112
|
+
* reloading the capped master playlist. The proxy can use this map to fetch the
|
|
113
|
+
* safe media playlist while preserving the browser-facing request URL. */
|
|
114
|
+
export function buildM3u8VariantFallbacks(playlist, baseUrl, maxBandwidth) {
|
|
115
|
+
const out = new Map();
|
|
116
|
+
if (maxBandwidth <= 0)
|
|
117
|
+
return out;
|
|
118
|
+
const variants = parseM3u8Variants(playlist, baseUrl).filter((v) => v.url.length > 0);
|
|
119
|
+
const allowed = variants
|
|
120
|
+
.filter((v) => v.bandwidth <= maxBandwidth)
|
|
121
|
+
.sort((a, b) => b.bandwidth - a.bandwidth);
|
|
122
|
+
const fallback = allowed[0];
|
|
123
|
+
if (!fallback)
|
|
124
|
+
return out;
|
|
125
|
+
for (const variant of variants) {
|
|
126
|
+
if (variant.bandwidth > maxBandwidth)
|
|
127
|
+
out.set(variant.url, fallback.url);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
110
131
|
/**
|
|
111
132
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
112
133
|
* playlist's own URL. Returns [] for master playlists (variant lists) — those
|
|
@@ -21,7 +21,7 @@ import https from "node:https";
|
|
|
21
21
|
import net from "node:net";
|
|
22
22
|
import tls from "node:tls";
|
|
23
23
|
import { egressConnect } from "./egress.js";
|
|
24
|
-
import { HlsPrefetcher, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments } from "./hls-prefetch.js";
|
|
24
|
+
import { HlsPrefetcher, buildM3u8VariantFallbacks, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments } from "./hls-prefetch.js";
|
|
25
25
|
import { RouteLearner } from "./route-learner.js";
|
|
26
26
|
import { GeoCnClassifier } from "./geo-cn.js";
|
|
27
27
|
import { MitmGateway } from "./mitm-gateway.js";
|
|
@@ -116,6 +116,11 @@ const EXIT_CONNECT_TIMEOUT_MS = 4000;
|
|
|
116
116
|
// underestimates by ~5x and poisons the ordering. Video segments clear this;
|
|
117
117
|
// API calls and redirects don't (they stay unmeasured = balanced by load).
|
|
118
118
|
const SPEED_MIN_BYTES = 512 * 1024;
|
|
119
|
+
// The 600 kbps CCTV fallback produces ~390-430 KB four-second segments. They
|
|
120
|
+
// are large enough to rank an exit, but never crossed the generic 512 KB gate,
|
|
121
|
+
// leaving every exit "unproven" and rotating foreground playback across slow
|
|
122
|
+
// paths. Keep the generic gate conservative and use this only for HLS bodies.
|
|
123
|
+
const HLS_SPEED_MIN_BYTES = 256 * 1024;
|
|
119
124
|
// We record PEAK 1-second throughput within a transfer, not lifetime average,
|
|
120
125
|
// so slow-start ramp-up doesn't drag the estimate down.
|
|
121
126
|
const SPEED_WINDOW_MS = 1000;
|
|
@@ -159,10 +164,15 @@ const NEAR_DEAD_EXIT_BPS = 60_000;
|
|
|
159
164
|
// threshold admitted 10.86.15.133 on a short peak even though its full CCTV
|
|
160
165
|
// segments averaged only 0.7-1.2 Mbit/s, causing each half-range to take 4-6s.
|
|
161
166
|
const HLS_DELIVERY_BPS = 200_000;
|
|
167
|
+
// Below this speed even the capped 0.9 Mbps stream has no useful headroom. If
|
|
168
|
+
// unproven exits remain, keep exploring them instead of locking startup onto
|
|
169
|
+
// the first measured-but-dead path (observed: 0.3 Mbps 15.133 hid the actually
|
|
170
|
+
// usable 134.139 for the remainder of the router process).
|
|
171
|
+
const HLS_STARTUP_EXPLORE_BPS = 100_000;
|
|
162
172
|
// Stability-first default for constrained exit pools. Set 0 to expose every
|
|
163
173
|
// origin variant, or 1800000 to restore CCTV 720P when another fast exit joins.
|
|
164
174
|
const HLS_MAX_VARIANT_BPS = process.env.AGENTNET_HLS_MAX_BPS === undefined
|
|
165
|
-
?
|
|
175
|
+
? 900_000
|
|
166
176
|
: Math.max(0, Number(process.env.AGENTNET_HLS_MAX_BPS));
|
|
167
177
|
export function isUsefulRecoverySample(peakBps, averageBps, minBps = HLS_DELIVERY_BPS) {
|
|
168
178
|
return peakBps >= minBps && averageBps >= minBps;
|
|
@@ -235,12 +245,14 @@ function floorExit(exit) {
|
|
|
235
245
|
/** Feed one tunnel's rate meter into the exit's speed EWMA (peak-window based).
|
|
236
246
|
* Only transfers past SPEED_MIN_BYTES count — smaller ones never reveal path
|
|
237
247
|
* capacity on a high-RTT link. */
|
|
238
|
-
function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS) {
|
|
248
|
+
function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS, minBytes = SPEED_MIN_BYTES, useWholeAverage = false) {
|
|
239
249
|
exit.bytesDown += meter.total;
|
|
240
|
-
if (meter.total <
|
|
250
|
+
if (meter.total < minBytes)
|
|
241
251
|
return;
|
|
242
|
-
const
|
|
243
|
-
|
|
252
|
+
const peakBps = meter.peakBps(durMs);
|
|
253
|
+
const averageBps = durMs > 0 ? (meter.total / durMs) * 1000 : 0;
|
|
254
|
+
const bps = useWholeAverage ? Math.min(peakBps, averageBps) : peakBps;
|
|
255
|
+
if (bps <= 0 || averageBps <= 0)
|
|
244
256
|
return;
|
|
245
257
|
if (exit.flooredAtMs !== null) {
|
|
246
258
|
// FLOOR_BPS is a penalty, not a real historical sample. A qualifying probe
|
|
@@ -251,8 +263,7 @@ function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS) {
|
|
|
251
263
|
// also prove whole-segment average throughput. A probe observed at a 0.9
|
|
252
264
|
// Mbit/s peak but only 0.6 Mbit/s average caused six-way prefetch/hedge
|
|
253
265
|
// congestion when it rejoined a 1.8 Mbit/s stream.
|
|
254
|
-
|
|
255
|
-
if (isUsefulRecoverySample(bps, averageBps, recoveryMinBps)) {
|
|
266
|
+
if (isUsefulRecoverySample(peakBps, averageBps, recoveryMinBps)) {
|
|
256
267
|
exit.speedEwma = bps;
|
|
257
268
|
exit.flooredAtMs = null;
|
|
258
269
|
}
|
|
@@ -287,10 +298,14 @@ export function selectHlsPrimaryPool(pool) {
|
|
|
287
298
|
// still safer than falling back to every unproven/degraded exit. This is the
|
|
288
299
|
// adaptive-bitrate path: the master playlist is capped separately, while
|
|
289
300
|
// foreground delivery stays on the least-bad known tier.
|
|
301
|
+
const unproven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma === null);
|
|
290
302
|
const measured = pool.filter((e) => e.flooredAtMs === null && e.speedEwma !== null);
|
|
291
|
-
if (measured.length > 0)
|
|
303
|
+
if (measured.length > 0) {
|
|
304
|
+
const bestMeasured = Math.max(...measured.map((e) => e.speedEwma ?? 0));
|
|
305
|
+
if (bestMeasured < HLS_STARTUP_EXPLORE_BPS && unproven.length > 0)
|
|
306
|
+
return unproven;
|
|
292
307
|
return fastestTier(measured);
|
|
293
|
-
|
|
308
|
+
}
|
|
294
309
|
return unproven.length > 0 ? unproven : pool;
|
|
295
310
|
}
|
|
296
311
|
/** Dynamic prefetch concurrency follows only the foreground-safe pool. */
|
|
@@ -1315,7 +1330,7 @@ export function startMultiExitRouter(opts) {
|
|
|
1315
1330
|
nextExit(`empty ${status}`);
|
|
1316
1331
|
return;
|
|
1317
1332
|
}
|
|
1318
|
-
sampleSpeed(exit, meter, durMs, HLS_DELIVERY_BPS);
|
|
1333
|
+
sampleSpeed(exit, meter, durMs, HLS_DELIVERY_BPS, HLS_SPEED_MIN_BYTES, true);
|
|
1319
1334
|
if (label) {
|
|
1320
1335
|
const kb = (body.length / 1024).toFixed(0);
|
|
1321
1336
|
const mbps = durMs > 0 ? ((body.length * 8) / durMs / 1000).toFixed(1) : "?";
|
|
@@ -1391,7 +1406,7 @@ export function startMultiExitRouter(opts) {
|
|
|
1391
1406
|
return false;
|
|
1392
1407
|
const now = Date.now();
|
|
1393
1408
|
return exits.some((e) => e.region === fallbackRegion && e.trippedUntil <= now &&
|
|
1394
|
-
e.flooredAtMs === null && e.speedEwma !== null);
|
|
1409
|
+
e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >= HLS_STARTUP_EXPLORE_BPS);
|
|
1395
1410
|
};
|
|
1396
1411
|
// Stripe count is a connection count, not a distinct-exit count. Keep three
|
|
1397
1412
|
// parallel ranges even when only one exit is qualified: two ranges still
|
|
@@ -1412,6 +1427,8 @@ export function startMultiExitRouter(opts) {
|
|
|
1412
1427
|
// several fetches on ONE exit divide it N ways and everything stalls).
|
|
1413
1428
|
// prefetchAhead=3 matches the 3-segment live window CCTV publishes.
|
|
1414
1429
|
const hls = new HlsPrefetcher(fetchViaRegionBuffered, { stripes: AGG, stripesFn: usableChinaExits, stripeChunkBytes: 256 * 1024, concurrencyFn: hlsConcurrency, prefetchAhead: 3 });
|
|
1430
|
+
const hlsVariantFallbacks = new Map();
|
|
1431
|
+
const loggedVariantFallbacks = new Set();
|
|
1415
1432
|
let lastHlsHits = 0;
|
|
1416
1433
|
function bumpHlsStats() {
|
|
1417
1434
|
const s = hls.stats;
|
|
@@ -1428,7 +1445,16 @@ export function startMultiExitRouter(opts) {
|
|
|
1428
1445
|
// else streams through one exit.
|
|
1429
1446
|
function serveHlsAware(o) {
|
|
1430
1447
|
const { fullUrl, host, port, path, target, method, headers, req, res, order, isTls } = o;
|
|
1431
|
-
|
|
1448
|
+
// A player may retain a high-bitrate media-playlist URL after the capped
|
|
1449
|
+
// master has been reloaded. Fetch the best allowed variant on its behalf;
|
|
1450
|
+
// the returned media playlist contains the safe segment names, so the next
|
|
1451
|
+
// request naturally moves to the lower bitrate without a page reload.
|
|
1452
|
+
const effectiveFullUrl = hlsVariantFallbacks.get(fullUrl) ?? fullUrl;
|
|
1453
|
+
const u = new URL(effectiveFullUrl);
|
|
1454
|
+
if (effectiveFullUrl !== fullUrl && !loggedVariantFallbacks.has(fullUrl)) {
|
|
1455
|
+
loggedVariantFallbacks.add(fullUrl);
|
|
1456
|
+
log(` HLS variant fallback ${fullUrl} → ${effectiveFullUrl}`);
|
|
1457
|
+
}
|
|
1432
1458
|
// The CCTV H5 player fetches segments via XHR/fetch (HLSP2P/DRM), so the
|
|
1433
1459
|
// browser enforces CORS. The CDN sends Access-Control-Allow-Origin, but our
|
|
1434
1460
|
// cache-hit and dedup-join serving paths only carried content-type — dropping
|
|
@@ -1496,20 +1522,23 @@ export function startMultiExitRouter(opts) {
|
|
|
1496
1522
|
let playlistVariants = [];
|
|
1497
1523
|
if (r.status === 200 && isPlaylist) {
|
|
1498
1524
|
playlistText = r.body.toString("utf8");
|
|
1499
|
-
playlistVariants = parseM3u8Variants(playlistText,
|
|
1525
|
+
playlistVariants = parseM3u8Variants(playlistText, effectiveFullUrl);
|
|
1500
1526
|
if (playlistVariants.length > 0 && HLS_MAX_VARIANT_BPS > 0) {
|
|
1501
1527
|
const originalCount = playlistVariants.length;
|
|
1528
|
+
for (const [blocked, fallback] of buildM3u8VariantFallbacks(playlistText, effectiveFullUrl, HLS_MAX_VARIANT_BPS)) {
|
|
1529
|
+
hlsVariantFallbacks.set(blocked, fallback);
|
|
1530
|
+
}
|
|
1502
1531
|
const capped = capM3u8Variants(playlistText, HLS_MAX_VARIANT_BPS);
|
|
1503
1532
|
if (capped !== playlistText) {
|
|
1504
1533
|
playlistText = capped;
|
|
1505
1534
|
r.body = Buffer.from(capped, "utf8");
|
|
1506
|
-
playlistVariants = parseM3u8Variants(capped,
|
|
1535
|
+
playlistVariants = parseM3u8Variants(capped, effectiveFullUrl);
|
|
1507
1536
|
log(` playlist ${host}: capped ${originalCount} variants to ${playlistVariants.length} at ${(HLS_MAX_VARIANT_BPS / 1e6).toFixed(1)}Mbps`);
|
|
1508
1537
|
}
|
|
1509
1538
|
}
|
|
1510
1539
|
else if (playlistVariants.length === 0) {
|
|
1511
|
-
const segs = parseM3u8Segments(playlistText,
|
|
1512
|
-
const primed = await hls.primePlaylist(
|
|
1540
|
+
const segs = parseM3u8Segments(playlistText, effectiveFullUrl);
|
|
1541
|
+
const primed = await hls.primePlaylist(effectiveFullUrl, playlistText, 8000, 2);
|
|
1513
1542
|
if (primed > 0)
|
|
1514
1543
|
log(` playlist ${host}: ${primed} segment(s) cached before first delivery`);
|
|
1515
1544
|
else
|
|
@@ -1531,11 +1560,11 @@ export function startMultiExitRouter(opts) {
|
|
|
1531
1560
|
const ct = r.headers["content-type"];
|
|
1532
1561
|
if (isPlaylist) {
|
|
1533
1562
|
const text = playlistText ?? r.body.toString("utf8");
|
|
1534
|
-
const variants = playlistVariants.length > 0 ? playlistVariants : parseM3u8Variants(text,
|
|
1563
|
+
const variants = playlistVariants.length > 0 ? playlistVariants : parseM3u8Variants(text, effectiveFullUrl);
|
|
1535
1564
|
if (variants.length > 0) {
|
|
1536
1565
|
const desc = variants.map((v) => `${v.resolution || "?"}@${(v.bandwidth / 1e6).toFixed(1)}Mbps`).join(", ");
|
|
1537
1566
|
log(` playlist ${host} is MASTER — variants: ${desc}`);
|
|
1538
|
-
hls.onPlaylist(
|
|
1567
|
+
hls.onPlaylist(effectiveFullUrl, text);
|
|
1539
1568
|
}
|
|
1540
1569
|
}
|
|
1541
1570
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.196",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|