@apocaliss92/nodelink-js 0.6.2 → 0.6.3

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.
@@ -3,7 +3,7 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-D4TKRGUP.js";
6
+ } from "../chunk-Q4AXRV2G.js";
7
7
  import "../chunk-IQVVVSXO.js";
8
8
  import {
9
9
  __require
package/dist/index.cjs CHANGED
@@ -8442,6 +8442,7 @@ __export(index_exports, {
8442
8442
  ReolinkHttpClient: () => ReolinkHttpClient,
8443
8443
  Rfc4571Muxer: () => Rfc4571Muxer,
8444
8444
  RtspBackchannel: () => RtspBackchannel,
8445
+ _clearP2pLookupDedupForTests: () => _clearP2pLookupDedupForTests,
8445
8446
  _resetEmailPushBusForTests: () => _resetEmailPushBusForTests,
8446
8447
  abilitiesHasAny: () => abilitiesHasAny,
8447
8448
  aesDecrypt: () => aesDecrypt,
@@ -8554,6 +8555,7 @@ __export(index_exports, {
8554
8555
  isH265Irap: () => isH265Irap,
8555
8556
  isH265KeyframeAnnexB: () => isH265KeyframeAnnexB,
8556
8557
  isNvrHubModel: () => isNvrHubModel,
8558
+ isSameSubnetAsAnyLocalIface: () => isSameSubnetAsAnyLocalIface,
8557
8559
  isTcpFailureThatShouldFallbackToUdp: () => isTcpFailureThatShouldFallbackToUdp,
8558
8560
  isUnroutableForP2P: () => isUnroutableForP2P,
8559
8561
  isValidH264AnnexBAccessUnit: () => isValidH264AnnexBAccessUnit,
@@ -8582,6 +8584,7 @@ __export(index_exports, {
8582
8584
  patchNestedTag: () => patchNestedTag,
8583
8585
  patchShelterXml: () => patchShelterXml,
8584
8586
  printNvrDiagnostics: () => printNvrDiagnostics,
8587
+ probeEgressForHost: () => probeEgressForHost,
8585
8588
  runAllDiagnosticsConsecutively: () => runAllDiagnosticsConsecutively,
8586
8589
  runMultifocalDiagnosticsConsecutively: () => runMultifocalDiagnosticsConsecutively,
8587
8590
  sampleStreams: () => sampleStreams,
@@ -9165,8 +9168,14 @@ async function getServerBinding(uid, options = {}) {
9165
9168
  headers: { Accept: "application/json" }
9166
9169
  });
9167
9170
  if (!res.ok) {
9171
+ let bodyPreview;
9172
+ try {
9173
+ const text = await res.text();
9174
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
9175
+ } catch {
9176
+ }
9168
9177
  logger?.log?.(
9169
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
9178
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
9170
9179
  );
9171
9180
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9172
9181
  return void 0;
@@ -9282,6 +9291,68 @@ function parseServerBindingResponse(raw) {
9282
9291
  }
9283
9292
 
9284
9293
  // src/bcudp/BcUdpStream.ts
9294
+ async function probeEgressForHost(destHost, destPort) {
9295
+ return await new Promise((resolve, reject) => {
9296
+ const probe = import_node_dgram.default.createSocket("udp4");
9297
+ let settled = false;
9298
+ const finish = (err, out) => {
9299
+ if (settled) return;
9300
+ settled = true;
9301
+ try {
9302
+ probe.close();
9303
+ } catch {
9304
+ }
9305
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
9306
+ else resolve(out);
9307
+ };
9308
+ probe.on("error", (e) => finish(e));
9309
+ try {
9310
+ probe.connect(destPort, destHost, () => {
9311
+ try {
9312
+ const a = probe.address();
9313
+ if (typeof a === "string") return finish(new Error("probe address is string"));
9314
+ finish(void 0, {
9315
+ localAddress: a.address,
9316
+ localPort: a.port
9317
+ });
9318
+ } catch (e) {
9319
+ finish(e);
9320
+ }
9321
+ });
9322
+ } catch (e) {
9323
+ finish(e);
9324
+ }
9325
+ });
9326
+ }
9327
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
9328
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
9329
+ const dest = destHost.split(".").map((s) => Number(s));
9330
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
9331
+ return "unknown";
9332
+ const ifaces = (0, import_node_os.networkInterfaces)();
9333
+ let ownerSubnet;
9334
+ for (const name of Object.keys(ifaces)) {
9335
+ const entries = ifaces[name];
9336
+ if (!entries) continue;
9337
+ for (const e of entries) {
9338
+ if (e.family !== "IPv4" || e.internal) continue;
9339
+ if (e.address !== srcInfo.localAddress) continue;
9340
+ const addr = e.address.split(".").map((s) => Number(s));
9341
+ const mask = e.netmask.split(".").map((s) => Number(s));
9342
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
9343
+ continue;
9344
+ ownerSubnet = { addr, mask };
9345
+ break;
9346
+ }
9347
+ if (ownerSubnet) break;
9348
+ }
9349
+ if (!ownerSubnet) return "unknown";
9350
+ for (let i = 0; i < 4; i++) {
9351
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
9352
+ return "mismatch";
9353
+ }
9354
+ return "same";
9355
+ }
9285
9356
  var AckLatency = class {
9286
9357
  currentValues = [];
9287
9358
  lastReceiveTime = null;
@@ -9360,6 +9431,16 @@ function isUnroutableForP2P(ip) {
9360
9431
  var P2P_LOOKUP_PORT = 9999;
9361
9432
  var P2P_MAX_WAIT_MS = 15e3;
9362
9433
  var P2P_RESEND_WAIT_MS = 500;
9434
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
9435
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
9436
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
9437
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
9438
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
9439
+ function _clearP2pLookupDedupForTests() {
9440
+ inflightP2pLookups.clear();
9441
+ cachedP2pLookups.clear();
9442
+ negCachedP2pLookups.clear();
9443
+ }
9363
9444
  var BcUdpStream = class extends import_node_events.EventEmitter {
9364
9445
  opts;
9365
9446
  /**
@@ -9448,31 +9529,14 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9448
9529
  });
9449
9530
  sock.on("error", (e) => this.emit("error", e));
9450
9531
  sock.on("close", () => this.emit("close"));
9451
- const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
9452
- for (let i = portRange.length - 1; i > 0; i--) {
9453
- const j = Math.floor(Math.random() * (i + 1));
9454
- [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
9455
- }
9456
- let bound = false;
9457
- for (const port of portRange) {
9458
- try {
9459
- await new Promise((resolve, reject) => {
9460
- sock.once("error", reject);
9461
- sock.bind(port, "0.0.0.0", () => {
9462
- sock.removeListener("error", reject);
9463
- resolve();
9464
- });
9465
- });
9466
- bound = true;
9467
- break;
9468
- } catch {
9469
- }
9470
- }
9471
- if (!bound) {
9472
- await new Promise(
9473
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
9474
- );
9475
- }
9532
+ await new Promise((resolve, reject) => {
9533
+ const onErr = (e) => reject(e);
9534
+ sock.once("error", onErr);
9535
+ sock.bind(0, "0.0.0.0", () => {
9536
+ sock.removeListener("error", onErr);
9537
+ resolve();
9538
+ });
9539
+ });
9476
9540
  if (this.opts.mode === "direct") {
9477
9541
  this.remote = { host: this.opts.host, port: this.opts.port };
9478
9542
  this.clientId = this.opts.clientId;
@@ -9543,6 +9607,49 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9543
9607
  this.remote = { host: connected.rhost, port: connected.rport };
9544
9608
  }
9545
9609
  async p2pUidLookup(sock, uid) {
9610
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
9611
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
9612
+ const cached = cachedP2pLookups.get(uid);
9613
+ if (cached && cached.expires > Date.now()) {
9614
+ log(
9615
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
9616
+ );
9617
+ return cached.result;
9618
+ }
9619
+ const negCached = negCachedP2pLookups.get(uid);
9620
+ if (negCached && negCached.expires > Date.now()) {
9621
+ const remaining = negCached.expires - Date.now();
9622
+ log(
9623
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
9624
+ );
9625
+ throw negCached.error;
9626
+ }
9627
+ const inflight = inflightP2pLookups.get(uid);
9628
+ if (inflight) {
9629
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
9630
+ return await inflight;
9631
+ }
9632
+ const work = this._doP2pUidLookupWork(sock, uid);
9633
+ inflightP2pLookups.set(uid, work);
9634
+ try {
9635
+ const result = await work;
9636
+ cachedP2pLookups.set(uid, {
9637
+ result,
9638
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
9639
+ });
9640
+ return result;
9641
+ } catch (e) {
9642
+ const err = e instanceof Error ? e : new Error(String(e));
9643
+ negCachedP2pLookups.set(uid, {
9644
+ error: err,
9645
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
9646
+ });
9647
+ throw err;
9648
+ } finally {
9649
+ inflightP2pLookups.delete(uid);
9650
+ }
9651
+ }
9652
+ async _doP2pUidLookupWork(sock, uid) {
9546
9653
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
9547
9654
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
9548
9655
  const t0 = Date.now();
@@ -10010,6 +10117,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
10010
10117
  log(
10011
10118
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
10012
10119
  );
10120
+ if (directHost && localMode === "local-direct") {
10121
+ try {
10122
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
10123
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
10124
+ if (sameSubnet === "mismatch") {
10125
+ log(
10126
+ `WARN: kernel-chosen source IP ${egress.localAddress} is NOT in the same subnet as ${directHost}. Some Reolink battery cams silently drop discovery packets with off-subnet source IPs. If discovery fails, check your routing table \u2014 likely a Tailscale / VPN / secondary NIC stealing the default route.`
10127
+ );
10128
+ } else {
10129
+ log(
10130
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
10131
+ );
10132
+ }
10133
+ } catch (e) {
10134
+ this.emit("debug", "egress_probe_failed", e);
10135
+ }
10136
+ }
10013
10137
  let bytesSent = 0;
10014
10138
  let pktsRecv = 0;
10015
10139
  sock.on("message", () => {
@@ -43563,6 +43687,7 @@ function buildInitialStatus(config) {
43563
43687
  ReolinkHttpClient,
43564
43688
  Rfc4571Muxer,
43565
43689
  RtspBackchannel,
43690
+ _clearP2pLookupDedupForTests,
43566
43691
  _resetEmailPushBusForTests,
43567
43692
  abilitiesHasAny,
43568
43693
  aesDecrypt,
@@ -43675,6 +43800,7 @@ function buildInitialStatus(config) {
43675
43800
  isH265Irap,
43676
43801
  isH265KeyframeAnnexB,
43677
43802
  isNvrHubModel,
43803
+ isSameSubnetAsAnyLocalIface,
43678
43804
  isTcpFailureThatShouldFallbackToUdp,
43679
43805
  isUnroutableForP2P,
43680
43806
  isValidH264AnnexBAccessUnit,
@@ -43703,6 +43829,7 @@ function buildInitialStatus(config) {
43703
43829
  patchNestedTag,
43704
43830
  patchShelterXml,
43705
43831
  printNvrDiagnostics,
43832
+ probeEgressForHost,
43706
43833
  runAllDiagnosticsConsecutively,
43707
43834
  runMultifocalDiagnosticsConsecutively,
43708
43835
  sampleStreams,