@decentnetwork/lan 0.1.194 → 0.1.195

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.
Binary file
Binary file
Binary file
Binary file
@@ -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,37 @@ 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.exe is callable from WSL and the per-user ROOT store needs no
1427
+ // admin (Windows pops a confirmation dialog the user must accept).
1428
+ const isWsl = existsSync("/proc/version") &&
1429
+ readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
1430
+ if (isWsl) {
1431
+ const winCopy = "/mnt/c/Users/Public/agentnet-mitm-ca.pem";
1432
+ const winPath = "C:\\Users\\Public\\agentnet-mitm-ca.pem";
1433
+ console.log("WSL detected — the browser lives on Windows, installing into the Windows user trust store.");
1434
+ try {
1435
+ copyFileSync(caCertPath, winCopy);
1436
+ console.log("A Windows security dialog will pop up — click Yes to trust the CA...");
1437
+ const w = spawnSync("certutil.exe", ["-user", "-addstore", "-f", "ROOT", winPath], { stdio: "inherit" });
1438
+ if (w.status === 0) {
1439
+ console.log("✓ Windows store updated. Chrome/Edge on Windows pick it up immediately (reload the page).");
1440
+ }
1441
+ else {
1442
+ console.log("Install failed or dialog declined — in a Windows terminal run:");
1443
+ console.log(` certutil -user -addstore -f ROOT "${winPath}"`);
1444
+ }
1445
+ }
1446
+ catch (e) {
1447
+ console.log(`Could not copy the cert to the Windows side (${e.message}). Manually:`);
1448
+ console.log(` cp "${caCertPath}" ${winCopy}`);
1449
+ console.log(` certutil.exe -user -addstore -f ROOT "${winPath}"`);
1450
+ }
1451
+ console.log("(Linux-side clients like curl inside WSL would additionally need the system CA store:");
1452
+ console.log(` sudo cp "${caCertPath}" /usr/local/share/ca-certificates/agentnet-mitm-ca.crt && sudo update-ca-certificates)`);
1453
+ return;
1454
+ }
1424
1455
  console.log("Installing into the system CA store (asks for your sudo password)...");
1425
1456
  const cp = spawnSync("sudo", ["cp", caCertPath, "/usr/local/share/ca-certificates/agentnet-mitm-ca.crt"], { stdio: "inherit" });
1426
1457
  const up = cp.status === 0 ? spawnSync("sudo", ["update-ca-certificates"], { stdio: "inherit" }) : cp;
@@ -1431,8 +1462,20 @@ export async function cmdProxyTrustCa(args) {
1431
1462
  console.log("(package: libnss3-tools). Firefox: Settings → Certificates → Import.");
1432
1463
  return;
1433
1464
  }
1434
- console.log("Windows: import the cert into 'Trusted Root Certification Authorities':");
1435
- console.log(` certutil -addstore -f ROOT "${caCertPath}"`);
1465
+ if (process.platform === "win32") {
1466
+ console.log("Installing into the Windows user trust store (a security dialog will pop up — click Yes)...");
1467
+ const w = spawnSync("certutil", ["-user", "-addstore", "-f", "ROOT", caCertPath], { stdio: "inherit" });
1468
+ if (w.status === 0) {
1469
+ console.log("✓ Trusted. Chrome/Edge pick it up immediately (reload the page).");
1470
+ }
1471
+ else {
1472
+ console.log("Install failed or dialog declined — run manually (admin terminal for the machine store):");
1473
+ console.log(` certutil -addstore -f ROOT "${caCertPath}"`);
1474
+ }
1475
+ return;
1476
+ }
1477
+ console.log(`Unsupported platform ${process.platform} — import this CA into your browser's trust store manually:`);
1478
+ console.log(` ${caCertPath}`);
1436
1479
  }
1437
1480
  export async function cmdProxyRouter(args) {
1438
1481
  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";
@@ -162,7 +162,7 @@ const HLS_DELIVERY_BPS = 200_000;
162
162
  // Stability-first default for constrained exit pools. Set 0 to expose every
163
163
  // origin variant, or 1800000 to restore CCTV 720P when another fast exit joins.
164
164
  const HLS_MAX_VARIANT_BPS = process.env.AGENTNET_HLS_MAX_BPS === undefined
165
- ? 1_400_000
165
+ ? 900_000
166
166
  : Math.max(0, Number(process.env.AGENTNET_HLS_MAX_BPS));
167
167
  export function isUsefulRecoverySample(peakBps, averageBps, minBps = HLS_DELIVERY_BPS) {
168
168
  return peakBps >= minBps && averageBps >= minBps;
@@ -1412,6 +1412,8 @@ export function startMultiExitRouter(opts) {
1412
1412
  // several fetches on ONE exit divide it N ways and everything stalls).
1413
1413
  // prefetchAhead=3 matches the 3-segment live window CCTV publishes.
1414
1414
  const hls = new HlsPrefetcher(fetchViaRegionBuffered, { stripes: AGG, stripesFn: usableChinaExits, stripeChunkBytes: 256 * 1024, concurrencyFn: hlsConcurrency, prefetchAhead: 3 });
1415
+ const hlsVariantFallbacks = new Map();
1416
+ const loggedVariantFallbacks = new Set();
1415
1417
  let lastHlsHits = 0;
1416
1418
  function bumpHlsStats() {
1417
1419
  const s = hls.stats;
@@ -1428,7 +1430,16 @@ export function startMultiExitRouter(opts) {
1428
1430
  // else streams through one exit.
1429
1431
  function serveHlsAware(o) {
1430
1432
  const { fullUrl, host, port, path, target, method, headers, req, res, order, isTls } = o;
1431
- const u = new URL(fullUrl);
1433
+ // A player may retain a high-bitrate media-playlist URL after the capped
1434
+ // master has been reloaded. Fetch the best allowed variant on its behalf;
1435
+ // the returned media playlist contains the safe segment names, so the next
1436
+ // request naturally moves to the lower bitrate without a page reload.
1437
+ const effectiveFullUrl = hlsVariantFallbacks.get(fullUrl) ?? fullUrl;
1438
+ const u = new URL(effectiveFullUrl);
1439
+ if (effectiveFullUrl !== fullUrl && !loggedVariantFallbacks.has(fullUrl)) {
1440
+ loggedVariantFallbacks.add(fullUrl);
1441
+ log(` HLS variant fallback ${fullUrl} → ${effectiveFullUrl}`);
1442
+ }
1432
1443
  // The CCTV H5 player fetches segments via XHR/fetch (HLSP2P/DRM), so the
1433
1444
  // browser enforces CORS. The CDN sends Access-Control-Allow-Origin, but our
1434
1445
  // cache-hit and dedup-join serving paths only carried content-type — dropping
@@ -1496,20 +1507,23 @@ export function startMultiExitRouter(opts) {
1496
1507
  let playlistVariants = [];
1497
1508
  if (r.status === 200 && isPlaylist) {
1498
1509
  playlistText = r.body.toString("utf8");
1499
- playlistVariants = parseM3u8Variants(playlistText, fullUrl);
1510
+ playlistVariants = parseM3u8Variants(playlistText, effectiveFullUrl);
1500
1511
  if (playlistVariants.length > 0 && HLS_MAX_VARIANT_BPS > 0) {
1501
1512
  const originalCount = playlistVariants.length;
1513
+ for (const [blocked, fallback] of buildM3u8VariantFallbacks(playlistText, effectiveFullUrl, HLS_MAX_VARIANT_BPS)) {
1514
+ hlsVariantFallbacks.set(blocked, fallback);
1515
+ }
1502
1516
  const capped = capM3u8Variants(playlistText, HLS_MAX_VARIANT_BPS);
1503
1517
  if (capped !== playlistText) {
1504
1518
  playlistText = capped;
1505
1519
  r.body = Buffer.from(capped, "utf8");
1506
- playlistVariants = parseM3u8Variants(capped, fullUrl);
1520
+ playlistVariants = parseM3u8Variants(capped, effectiveFullUrl);
1507
1521
  log(` playlist ${host}: capped ${originalCount} variants to ${playlistVariants.length} at ${(HLS_MAX_VARIANT_BPS / 1e6).toFixed(1)}Mbps`);
1508
1522
  }
1509
1523
  }
1510
1524
  else if (playlistVariants.length === 0) {
1511
- const segs = parseM3u8Segments(playlistText, fullUrl);
1512
- const primed = await hls.primePlaylist(fullUrl, playlistText, 8000, 2);
1525
+ const segs = parseM3u8Segments(playlistText, effectiveFullUrl);
1526
+ const primed = await hls.primePlaylist(effectiveFullUrl, playlistText, 8000, 2);
1513
1527
  if (primed > 0)
1514
1528
  log(` playlist ${host}: ${primed} segment(s) cached before first delivery`);
1515
1529
  else
@@ -1531,11 +1545,11 @@ export function startMultiExitRouter(opts) {
1531
1545
  const ct = r.headers["content-type"];
1532
1546
  if (isPlaylist) {
1533
1547
  const text = playlistText ?? r.body.toString("utf8");
1534
- const variants = playlistVariants.length > 0 ? playlistVariants : parseM3u8Variants(text, fullUrl);
1548
+ const variants = playlistVariants.length > 0 ? playlistVariants : parseM3u8Variants(text, effectiveFullUrl);
1535
1549
  if (variants.length > 0) {
1536
1550
  const desc = variants.map((v) => `${v.resolution || "?"}@${(v.bandwidth / 1e6).toFixed(1)}Mbps`).join(", ");
1537
1551
  log(` playlist ${host} is MASTER — variants: ${desc}`);
1538
- hls.onPlaylist(fullUrl, text);
1552
+ hls.onPlaylist(effectiveFullUrl, text);
1539
1553
  }
1540
1554
  }
1541
1555
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.194",
3
+ "version": "0.1.195",
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",