@decentnetwork/lan 0.1.195 → 0.1.197
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 +12 -9
- package/dist/proxy/hls-prefetch.d.ts +8 -0
- package/dist/proxy/hls-prefetch.js +25 -0
- package/dist/proxy/multi-exit-router.js +83 -44
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.js
CHANGED
|
@@ -1423,8 +1423,9 @@ export async function cmdProxyTrustCa(args) {
|
|
|
1423
1423
|
if (process.platform === "linux") {
|
|
1424
1424
|
// WSL: the browser runs on the WINDOWS side and trusts the WINDOWS cert
|
|
1425
1425
|
// store — installing into this distro's Linux store does nothing for it.
|
|
1426
|
-
// certutil
|
|
1427
|
-
//
|
|
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).
|
|
1428
1429
|
const isWsl = existsSync("/proc/version") &&
|
|
1429
1430
|
readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
|
|
1430
1431
|
if (isWsl) {
|
|
@@ -1434,19 +1435,20 @@ export async function cmdProxyTrustCa(args) {
|
|
|
1434
1435
|
try {
|
|
1435
1436
|
copyFileSync(caCertPath, winCopy);
|
|
1436
1437
|
console.log("A Windows security dialog will pop up — click Yes to trust the CA...");
|
|
1437
|
-
const w = spawnSync("
|
|
1438
|
+
const w = spawnSync("powershell.exe", ["-NoProfile", "-Command", `Import-Certificate -FilePath '${winPath}' -CertStoreLocation Cert:\\CurrentUser\\Root`], { stdio: "inherit" });
|
|
1438
1439
|
if (w.status === 0) {
|
|
1439
1440
|
console.log("✓ Windows store updated. Chrome/Edge on Windows pick it up immediately (reload the page).");
|
|
1440
1441
|
}
|
|
1441
1442
|
else {
|
|
1442
|
-
console.log("Install failed or dialog declined — in a Windows
|
|
1443
|
-
console.log(`
|
|
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}")`);
|
|
1444
1446
|
}
|
|
1445
1447
|
}
|
|
1446
1448
|
catch (e) {
|
|
1447
1449
|
console.log(`Could not copy the cert to the Windows side (${e.message}). Manually:`);
|
|
1448
1450
|
console.log(` cp "${caCertPath}" ${winCopy}`);
|
|
1449
|
-
console.log(`
|
|
1451
|
+
console.log(` then in Windows PowerShell: Import-Certificate -FilePath "${winPath}" -CertStoreLocation Cert:\\CurrentUser\\Root`);
|
|
1450
1452
|
}
|
|
1451
1453
|
console.log("(Linux-side clients like curl inside WSL would additionally need the system CA store:");
|
|
1452
1454
|
console.log(` sudo cp "${caCertPath}" /usr/local/share/ca-certificates/agentnet-mitm-ca.crt && sudo update-ca-certificates)`);
|
|
@@ -1464,13 +1466,14 @@ export async function cmdProxyTrustCa(args) {
|
|
|
1464
1466
|
}
|
|
1465
1467
|
if (process.platform === "win32") {
|
|
1466
1468
|
console.log("Installing into the Windows user trust store (a security dialog will pop up — click Yes)...");
|
|
1467
|
-
const w = spawnSync("
|
|
1469
|
+
const w = spawnSync("powershell.exe", ["-NoProfile", "-Command", `Import-Certificate -FilePath '${caCertPath}' -CertStoreLocation Cert:\\CurrentUser\\Root`], { stdio: "inherit" });
|
|
1468
1470
|
if (w.status === 0) {
|
|
1469
1471
|
console.log("✓ Trusted. Chrome/Edge pick it up immediately (reload the page).");
|
|
1470
1472
|
}
|
|
1471
1473
|
else {
|
|
1472
|
-
console.log("Install failed or dialog declined —
|
|
1473
|
-
console.log(`
|
|
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}")`);
|
|
1474
1477
|
}
|
|
1475
1478
|
return;
|
|
1476
1479
|
}
|
|
@@ -29,6 +29,14 @@ export declare function capM3u8Variants(playlist: string, maxBandwidth: number):
|
|
|
29
29
|
* reloading the capped master playlist. The proxy can use this map to fetch the
|
|
30
30
|
* safe media playlist while preserving the browser-facing request URL. */
|
|
31
31
|
export declare function buildM3u8VariantFallbacks(playlist: string, baseUrl: string, maxBandwidth: number): Map<string, string>;
|
|
32
|
+
/** Resolve one HTTP byte-range against a complete cached body. Multiple ranges
|
|
33
|
+
* are intentionally rejected (the CCTV player uses only a single range). */
|
|
34
|
+
export declare function sliceHttpByteRange(body: Buffer, header: string): {
|
|
35
|
+
body: Buffer;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
total: number;
|
|
39
|
+
} | null;
|
|
32
40
|
/**
|
|
33
41
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
34
42
|
* playlist's own URL. Returns [] for master playlists (variant lists) — those
|
|
@@ -128,6 +128,31 @@ export function buildM3u8VariantFallbacks(playlist, baseUrl, maxBandwidth) {
|
|
|
128
128
|
}
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
|
+
/** Resolve one HTTP byte-range against a complete cached body. Multiple ranges
|
|
132
|
+
* are intentionally rejected (the CCTV player uses only a single range). */
|
|
133
|
+
export function sliceHttpByteRange(body, header) {
|
|
134
|
+
const match = /^bytes=(\d*)-(\d*)$/i.exec(header.trim());
|
|
135
|
+
if (!match || (match[1] === "" && match[2] === "") || body.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const total = body.length;
|
|
138
|
+
let start;
|
|
139
|
+
let end;
|
|
140
|
+
if (match[1] === "") {
|
|
141
|
+
const suffix = Number(match[2]);
|
|
142
|
+
if (!Number.isSafeInteger(suffix) || suffix <= 0)
|
|
143
|
+
return null;
|
|
144
|
+
start = Math.max(0, total - suffix);
|
|
145
|
+
end = total - 1;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
start = Number(match[1]);
|
|
149
|
+
end = match[2] === "" ? total - 1 : Number(match[2]);
|
|
150
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= total || end < start)
|
|
151
|
+
return null;
|
|
152
|
+
end = Math.min(end, total - 1);
|
|
153
|
+
}
|
|
154
|
+
return { body: body.subarray(start, end + 1), start, end, total };
|
|
155
|
+
}
|
|
131
156
|
/**
|
|
132
157
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
133
158
|
* 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, buildM3u8VariantFallbacks, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments } from "./hls-prefetch.js";
|
|
24
|
+
import { HlsPrefetcher, buildM3u8VariantFallbacks, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments, sliceHttpByteRange } 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,16 @@ 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;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
167
|
+
// Below this speed even a 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;
|
|
172
|
+
// Quality selection belongs to the player/user by default. Operators may set a
|
|
173
|
+
// positive value to impose an explicit site-local ceiling; 0 exposes every
|
|
174
|
+
// origin variant.
|
|
164
175
|
const HLS_MAX_VARIANT_BPS = process.env.AGENTNET_HLS_MAX_BPS === undefined
|
|
165
|
-
?
|
|
176
|
+
? 0
|
|
166
177
|
: Math.max(0, Number(process.env.AGENTNET_HLS_MAX_BPS));
|
|
167
178
|
export function isUsefulRecoverySample(peakBps, averageBps, minBps = HLS_DELIVERY_BPS) {
|
|
168
179
|
return peakBps >= minBps && averageBps >= minBps;
|
|
@@ -235,12 +246,14 @@ function floorExit(exit) {
|
|
|
235
246
|
/** Feed one tunnel's rate meter into the exit's speed EWMA (peak-window based).
|
|
236
247
|
* Only transfers past SPEED_MIN_BYTES count — smaller ones never reveal path
|
|
237
248
|
* capacity on a high-RTT link. */
|
|
238
|
-
function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS) {
|
|
249
|
+
function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS, minBytes = SPEED_MIN_BYTES, useWholeAverage = false) {
|
|
239
250
|
exit.bytesDown += meter.total;
|
|
240
|
-
if (meter.total <
|
|
251
|
+
if (meter.total < minBytes)
|
|
241
252
|
return;
|
|
242
|
-
const
|
|
243
|
-
|
|
253
|
+
const peakBps = meter.peakBps(durMs);
|
|
254
|
+
const averageBps = durMs > 0 ? (meter.total / durMs) * 1000 : 0;
|
|
255
|
+
const bps = useWholeAverage ? Math.min(peakBps, averageBps) : peakBps;
|
|
256
|
+
if (bps <= 0 || averageBps <= 0)
|
|
244
257
|
return;
|
|
245
258
|
if (exit.flooredAtMs !== null) {
|
|
246
259
|
// FLOOR_BPS is a penalty, not a real historical sample. A qualifying probe
|
|
@@ -251,8 +264,7 @@ function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS) {
|
|
|
251
264
|
// also prove whole-segment average throughput. A probe observed at a 0.9
|
|
252
265
|
// Mbit/s peak but only 0.6 Mbit/s average caused six-way prefetch/hedge
|
|
253
266
|
// congestion when it rejoined a 1.8 Mbit/s stream.
|
|
254
|
-
|
|
255
|
-
if (isUsefulRecoverySample(bps, averageBps, recoveryMinBps)) {
|
|
267
|
+
if (isUsefulRecoverySample(peakBps, averageBps, recoveryMinBps)) {
|
|
256
268
|
exit.speedEwma = bps;
|
|
257
269
|
exit.flooredAtMs = null;
|
|
258
270
|
}
|
|
@@ -287,10 +299,14 @@ export function selectHlsPrimaryPool(pool) {
|
|
|
287
299
|
// still safer than falling back to every unproven/degraded exit. This is the
|
|
288
300
|
// adaptive-bitrate path: the master playlist is capped separately, while
|
|
289
301
|
// foreground delivery stays on the least-bad known tier.
|
|
302
|
+
const unproven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma === null);
|
|
290
303
|
const measured = pool.filter((e) => e.flooredAtMs === null && e.speedEwma !== null);
|
|
291
|
-
if (measured.length > 0)
|
|
304
|
+
if (measured.length > 0) {
|
|
305
|
+
const bestMeasured = Math.max(...measured.map((e) => e.speedEwma ?? 0));
|
|
306
|
+
if (bestMeasured < HLS_STARTUP_EXPLORE_BPS && unproven.length > 0)
|
|
307
|
+
return unproven;
|
|
292
308
|
return fastestTier(measured);
|
|
293
|
-
|
|
309
|
+
}
|
|
294
310
|
return unproven.length > 0 ? unproven : pool;
|
|
295
311
|
}
|
|
296
312
|
/** Dynamic prefetch concurrency follows only the foreground-safe pool. */
|
|
@@ -1315,7 +1331,7 @@ export function startMultiExitRouter(opts) {
|
|
|
1315
1331
|
nextExit(`empty ${status}`);
|
|
1316
1332
|
return;
|
|
1317
1333
|
}
|
|
1318
|
-
sampleSpeed(exit, meter, durMs, HLS_DELIVERY_BPS);
|
|
1334
|
+
sampleSpeed(exit, meter, durMs, HLS_DELIVERY_BPS, HLS_SPEED_MIN_BYTES, true);
|
|
1319
1335
|
if (label) {
|
|
1320
1336
|
const kb = (body.length / 1024).toFixed(0);
|
|
1321
1337
|
const mbps = durMs > 0 ? ((body.length * 8) / durMs / 1000).toFixed(1) : "?";
|
|
@@ -1361,17 +1377,14 @@ export function startMultiExitRouter(opts) {
|
|
|
1361
1377
|
tryIdx(0);
|
|
1362
1378
|
});
|
|
1363
1379
|
}
|
|
1364
|
-
// Striping
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
// split fetching the same segment twice. concurrency:2 keeps at most 2
|
|
1373
|
-
// segments (≈6 stripes over ~3 exits) in flight so the pool isn't overwhelmed.
|
|
1374
|
-
const AGG = process.env.AGENTNET_HLS_STRIPES ? Math.max(1, Number(process.env.AGENTNET_HLS_STRIPES)) : 3;
|
|
1380
|
+
// Striping can aggregate genuinely independent exits, but it is harmful when
|
|
1381
|
+
// every range converges on one useful shared exit. Three stripes, a hedge per
|
|
1382
|
+
// stripe, and retries produced 6-11 simultaneous requests to 10.86.134.139;
|
|
1383
|
+
// its measured throughput collapsed from ~2 Mbps to ~0.3 Mbps and every
|
|
1384
|
+
// client sharing that exit stalled. Default to one whole-segment fetch (with
|
|
1385
|
+
// the existing single late hedge). Operators may explicitly opt into more
|
|
1386
|
+
// stripes after proving their exits have independent capacity.
|
|
1387
|
+
const AGG = process.env.AGENTNET_HLS_STRIPES ? Math.max(1, Number(process.env.AGENTNET_HLS_STRIPES)) : 1;
|
|
1375
1388
|
// How many china exits are ACTUALLY delivering right now — healthy (or with
|
|
1376
1389
|
// live traffic), not tripped, and not speed-penalized (an exit that returned
|
|
1377
1390
|
// empty/0-byte gets its score floored, so it drops out here until it serves
|
|
@@ -1391,16 +1404,13 @@ export function startMultiExitRouter(opts) {
|
|
|
1391
1404
|
return false;
|
|
1392
1405
|
const now = Date.now();
|
|
1393
1406
|
return exits.some((e) => e.region === fallbackRegion && e.trippedUntil <= now &&
|
|
1394
|
-
e.flooredAtMs === null && e.speedEwma !== null);
|
|
1407
|
+
e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >= HLS_STARTUP_EXPLORE_BPS);
|
|
1395
1408
|
};
|
|
1396
|
-
// Stripe count is a connection count, not a distinct-exit count.
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
// exit has delivered a real fast sample; otherwise the unproven startup pool
|
|
1402
|
-
// would scatter half-ranges onto every slow exit and hold assembly hostage.
|
|
1403
|
-
// Set AGENTNET_HLS_STRIPES=1 for an emergency rollback.
|
|
1409
|
+
// Stripe count is a connection count, not a distinct-exit count. During
|
|
1410
|
+
// startup, keep whole-segment mode until at least one exit has delivered a
|
|
1411
|
+
// real fast sample; otherwise the unproven startup pool would scatter ranges
|
|
1412
|
+
// onto every slow exit and hold assembly hostage. AGENTNET_HLS_STRIPES=N is
|
|
1413
|
+
// an explicit opt-in for pools where N independent exits are proven useful.
|
|
1404
1414
|
const usableChinaExits = () => AGG <= 1 || !hasMeasuredChinaExit() ? 1 : AGG;
|
|
1405
1415
|
const hlsConcurrency = () => hasMeasuredChinaExit() ? deliveringChinaExits() : 1;
|
|
1406
1416
|
// Prefetch parallelism = the number of exits ACTUALLY delivering right now
|
|
@@ -1468,23 +1478,36 @@ export function startMultiExitRouter(opts) {
|
|
|
1468
1478
|
res.end();
|
|
1469
1479
|
return;
|
|
1470
1480
|
}
|
|
1471
|
-
const
|
|
1481
|
+
const isPlaylist = looksLikePlaylist(u.pathname || "");
|
|
1482
|
+
const isSegment = looksLikeSegment(u.pathname || "");
|
|
1483
|
+
const requestedRange = typeof headers["range"] === "string" ? headers["range"] : null;
|
|
1484
|
+
const cacheable = method === "GET";
|
|
1472
1485
|
if (cacheable) {
|
|
1473
1486
|
const hit = hls.lookup(fullUrl);
|
|
1474
1487
|
if (hit) {
|
|
1488
|
+
const ranged = requestedRange && isSegment ? sliceHttpByteRange(hit.body, requestedRange) : null;
|
|
1489
|
+
if (requestedRange && isSegment && !ranged) {
|
|
1490
|
+
const hh = { "content-range": `bytes */${hit.body.length}`, "content-length": 0 };
|
|
1491
|
+
addCors(hh);
|
|
1492
|
+
res.writeHead(416, hh);
|
|
1493
|
+
res.end();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const body = ranged?.body ?? hit.body;
|
|
1475
1497
|
const hh = {
|
|
1476
1498
|
"content-type": hit.contentType ?? "application/octet-stream",
|
|
1477
|
-
"content-length":
|
|
1499
|
+
"content-length": body.length,
|
|
1478
1500
|
"x-agentnet-cache": "hit",
|
|
1501
|
+
"accept-ranges": "bytes",
|
|
1479
1502
|
};
|
|
1503
|
+
if (ranged)
|
|
1504
|
+
hh["content-range"] = `bytes ${ranged.start}-${ranged.end}/${ranged.total}`;
|
|
1480
1505
|
addCors(hh);
|
|
1481
|
-
res.writeHead(200, hh);
|
|
1482
|
-
res.end(
|
|
1506
|
+
res.writeHead(ranged ? 206 : 200, hh);
|
|
1507
|
+
res.end(body);
|
|
1483
1508
|
bumpHlsStats();
|
|
1484
1509
|
return;
|
|
1485
1510
|
}
|
|
1486
|
-
const isPlaylist = looksLikePlaylist(u.pathname || "");
|
|
1487
|
-
const isSegment = looksLikeSegment(u.pathname || "");
|
|
1488
1511
|
if (isPlaylist || isSegment) {
|
|
1489
1512
|
const fwd = {};
|
|
1490
1513
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -1530,17 +1553,33 @@ export function startMultiExitRouter(opts) {
|
|
|
1530
1553
|
log(` playlist ${host} is MEDIA — ${segs.length} segments, prefetching ahead`);
|
|
1531
1554
|
}
|
|
1532
1555
|
}
|
|
1533
|
-
|
|
1556
|
+
let responseStatus = r.status;
|
|
1557
|
+
let responseBody = r.body;
|
|
1558
|
+
const outHeaders = { ...r.headers };
|
|
1559
|
+
if (requestedRange && isSegment && r.status === 200) {
|
|
1560
|
+
const ranged = sliceHttpByteRange(r.body, requestedRange);
|
|
1561
|
+
if (!ranged) {
|
|
1562
|
+
responseStatus = 416;
|
|
1563
|
+
responseBody = Buffer.alloc(0);
|
|
1564
|
+
outHeaders["content-range"] = `bytes */${r.body.length}`;
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
responseStatus = 206;
|
|
1568
|
+
responseBody = ranged.body;
|
|
1569
|
+
outHeaders["content-range"] = `bytes ${ranged.start}-${ranged.end}/${ranged.total}`;
|
|
1570
|
+
outHeaders["accept-ranges"] = "bytes";
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
log(` MITM ${method} ${host}${(path || "/").slice(0, 80)} → ${responseStatus} (${responseBody.length}B)`);
|
|
1534
1574
|
if (r.status >= 400) {
|
|
1535
1575
|
const snip = r.body.slice(0, 300).toString("utf8").replace(/\s+/g, " ");
|
|
1536
1576
|
log(` body: ${snip}`);
|
|
1537
1577
|
}
|
|
1538
|
-
const outHeaders = { ...r.headers };
|
|
1539
1578
|
delete outHeaders["transfer-encoding"];
|
|
1540
|
-
outHeaders["content-length"] =
|
|
1579
|
+
outHeaders["content-length"] = responseBody.length;
|
|
1541
1580
|
addCors(outHeaders); // cache/dedup/striped paths drop the CDN's CORS header — always re-add
|
|
1542
|
-
res.writeHead(
|
|
1543
|
-
res.end(
|
|
1581
|
+
res.writeHead(responseStatus, outHeaders);
|
|
1582
|
+
res.end(responseBody);
|
|
1544
1583
|
if (r.status === 200) {
|
|
1545
1584
|
const ct = r.headers["content-type"];
|
|
1546
1585
|
if (isPlaylist) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.197",
|
|
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",
|