@apocaliss92/nodelink-js 0.6.2 → 0.6.4

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-UL34MR4L.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,
@@ -9123,6 +9126,7 @@ function readCache(uid, now) {
9123
9126
  }
9124
9127
  async function getServerBinding(uid, options = {}) {
9125
9128
  if (!uid || typeof uid !== "string") return void 0;
9129
+ uid = uid.toUpperCase();
9126
9130
  const now = Date.now();
9127
9131
  const cached = readCache(uid, now);
9128
9132
  if (cached?.kind === "ok") return cached.response;
@@ -9165,8 +9169,14 @@ async function getServerBinding(uid, options = {}) {
9165
9169
  headers: { Accept: "application/json" }
9166
9170
  });
9167
9171
  if (!res.ok) {
9172
+ let bodyPreview;
9173
+ try {
9174
+ const text = await res.text();
9175
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
9176
+ } catch {
9177
+ }
9168
9178
  logger?.log?.(
9169
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
9179
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
9170
9180
  );
9171
9181
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9172
9182
  return void 0;
@@ -9282,6 +9292,68 @@ function parseServerBindingResponse(raw) {
9282
9292
  }
9283
9293
 
9284
9294
  // src/bcudp/BcUdpStream.ts
9295
+ async function probeEgressForHost(destHost, destPort) {
9296
+ return await new Promise((resolve, reject) => {
9297
+ const probe = import_node_dgram.default.createSocket("udp4");
9298
+ let settled = false;
9299
+ const finish = (err, out) => {
9300
+ if (settled) return;
9301
+ settled = true;
9302
+ try {
9303
+ probe.close();
9304
+ } catch {
9305
+ }
9306
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
9307
+ else resolve(out);
9308
+ };
9309
+ probe.on("error", (e) => finish(e));
9310
+ try {
9311
+ probe.connect(destPort, destHost, () => {
9312
+ try {
9313
+ const a = probe.address();
9314
+ if (typeof a === "string") return finish(new Error("probe address is string"));
9315
+ finish(void 0, {
9316
+ localAddress: a.address,
9317
+ localPort: a.port
9318
+ });
9319
+ } catch (e) {
9320
+ finish(e);
9321
+ }
9322
+ });
9323
+ } catch (e) {
9324
+ finish(e);
9325
+ }
9326
+ });
9327
+ }
9328
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
9329
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
9330
+ const dest = destHost.split(".").map((s) => Number(s));
9331
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
9332
+ return "unknown";
9333
+ const ifaces = (0, import_node_os.networkInterfaces)();
9334
+ let ownerSubnet;
9335
+ for (const name of Object.keys(ifaces)) {
9336
+ const entries = ifaces[name];
9337
+ if (!entries) continue;
9338
+ for (const e of entries) {
9339
+ if (e.family !== "IPv4" || e.internal) continue;
9340
+ if (e.address !== srcInfo.localAddress) continue;
9341
+ const addr = e.address.split(".").map((s) => Number(s));
9342
+ const mask = e.netmask.split(".").map((s) => Number(s));
9343
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
9344
+ continue;
9345
+ ownerSubnet = { addr, mask };
9346
+ break;
9347
+ }
9348
+ if (ownerSubnet) break;
9349
+ }
9350
+ if (!ownerSubnet) return "unknown";
9351
+ for (let i = 0; i < 4; i++) {
9352
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
9353
+ return "mismatch";
9354
+ }
9355
+ return "same";
9356
+ }
9285
9357
  var AckLatency = class {
9286
9358
  currentValues = [];
9287
9359
  lastReceiveTime = null;
@@ -9360,6 +9432,16 @@ function isUnroutableForP2P(ip) {
9360
9432
  var P2P_LOOKUP_PORT = 9999;
9361
9433
  var P2P_MAX_WAIT_MS = 15e3;
9362
9434
  var P2P_RESEND_WAIT_MS = 500;
9435
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
9436
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
9437
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
9438
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
9439
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
9440
+ function _clearP2pLookupDedupForTests() {
9441
+ inflightP2pLookups.clear();
9442
+ cachedP2pLookups.clear();
9443
+ negCachedP2pLookups.clear();
9444
+ }
9363
9445
  var BcUdpStream = class extends import_node_events.EventEmitter {
9364
9446
  opts;
9365
9447
  /**
@@ -9448,31 +9530,14 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9448
9530
  });
9449
9531
  sock.on("error", (e) => this.emit("error", e));
9450
9532
  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
- }
9533
+ await new Promise((resolve, reject) => {
9534
+ const onErr = (e) => reject(e);
9535
+ sock.once("error", onErr);
9536
+ sock.bind(0, "0.0.0.0", () => {
9537
+ sock.removeListener("error", onErr);
9538
+ resolve();
9539
+ });
9540
+ });
9476
9541
  if (this.opts.mode === "direct") {
9477
9542
  this.remote = { host: this.opts.host, port: this.opts.port };
9478
9543
  this.clientId = this.opts.clientId;
@@ -9543,6 +9608,49 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9543
9608
  this.remote = { host: connected.rhost, port: connected.rport };
9544
9609
  }
9545
9610
  async p2pUidLookup(sock, uid) {
9611
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
9612
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
9613
+ const cached = cachedP2pLookups.get(uid);
9614
+ if (cached && cached.expires > Date.now()) {
9615
+ log(
9616
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
9617
+ );
9618
+ return cached.result;
9619
+ }
9620
+ const negCached = negCachedP2pLookups.get(uid);
9621
+ if (negCached && negCached.expires > Date.now()) {
9622
+ const remaining = negCached.expires - Date.now();
9623
+ log(
9624
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
9625
+ );
9626
+ throw negCached.error;
9627
+ }
9628
+ const inflight = inflightP2pLookups.get(uid);
9629
+ if (inflight) {
9630
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
9631
+ return await inflight;
9632
+ }
9633
+ const work = this._doP2pUidLookupWork(sock, uid);
9634
+ inflightP2pLookups.set(uid, work);
9635
+ try {
9636
+ const result = await work;
9637
+ cachedP2pLookups.set(uid, {
9638
+ result,
9639
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
9640
+ });
9641
+ return result;
9642
+ } catch (e) {
9643
+ const err = e instanceof Error ? e : new Error(String(e));
9644
+ negCachedP2pLookups.set(uid, {
9645
+ error: err,
9646
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
9647
+ });
9648
+ throw err;
9649
+ } finally {
9650
+ inflightP2pLookups.delete(uid);
9651
+ }
9652
+ }
9653
+ async _doP2pUidLookupWork(sock, uid) {
9546
9654
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
9547
9655
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
9548
9656
  const t0 = Date.now();
@@ -10010,6 +10118,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
10010
10118
  log(
10011
10119
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
10012
10120
  );
10121
+ if (directHost && localMode === "local-direct") {
10122
+ try {
10123
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
10124
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
10125
+ if (sameSubnet === "mismatch") {
10126
+ log(
10127
+ `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.`
10128
+ );
10129
+ } else {
10130
+ log(
10131
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
10132
+ );
10133
+ }
10134
+ } catch (e) {
10135
+ this.emit("debug", "egress_probe_failed", e);
10136
+ }
10137
+ }
10013
10138
  let bytesSent = 0;
10014
10139
  let pktsRecv = 0;
10015
10140
  sock.on("message", () => {
@@ -40853,7 +40978,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
40853
40978
  return methods.filter((m) => m === "local-direct");
40854
40979
  }
40855
40980
  function normalizeUid(uid) {
40856
- const v = uid?.trim();
40981
+ const v = uid?.trim().toUpperCase();
40857
40982
  return v ? v : void 0;
40858
40983
  }
40859
40984
  function maskUid(uid) {
@@ -43563,6 +43688,7 @@ function buildInitialStatus(config) {
43563
43688
  ReolinkHttpClient,
43564
43689
  Rfc4571Muxer,
43565
43690
  RtspBackchannel,
43691
+ _clearP2pLookupDedupForTests,
43566
43692
  _resetEmailPushBusForTests,
43567
43693
  abilitiesHasAny,
43568
43694
  aesDecrypt,
@@ -43675,6 +43801,7 @@ function buildInitialStatus(config) {
43675
43801
  isH265Irap,
43676
43802
  isH265KeyframeAnnexB,
43677
43803
  isNvrHubModel,
43804
+ isSameSubnetAsAnyLocalIface,
43678
43805
  isTcpFailureThatShouldFallbackToUdp,
43679
43806
  isUnroutableForP2P,
43680
43807
  isValidH264AnnexBAccessUnit,
@@ -43703,6 +43830,7 @@ function buildInitialStatus(config) {
43703
43830
  patchNestedTag,
43704
43831
  patchShelterXml,
43705
43832
  printNvrDiagnostics,
43833
+ probeEgressForHost,
43706
43834
  runAllDiagnosticsConsecutively,
43707
43835
  runMultifocalDiagnosticsConsecutively,
43708
43836
  sampleStreams,