@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.
@@ -11340,6 +11340,7 @@ function readCache(uid, now) {
11340
11340
  }
11341
11341
  async function getServerBinding(uid, options = {}) {
11342
11342
  if (!uid || typeof uid !== "string") return void 0;
11343
+ uid = uid.toUpperCase();
11343
11344
  const now = Date.now();
11344
11345
  const cached = readCache(uid, now);
11345
11346
  if (cached?.kind === "ok") return cached.response;
@@ -11382,8 +11383,14 @@ async function getServerBinding(uid, options = {}) {
11382
11383
  headers: { Accept: "application/json" }
11383
11384
  });
11384
11385
  if (!res.ok) {
11386
+ let bodyPreview;
11387
+ try {
11388
+ const text = await res.text();
11389
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
11390
+ } catch {
11391
+ }
11385
11392
  logger?.log?.(
11386
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
11393
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
11387
11394
  );
11388
11395
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11389
11396
  return void 0;
@@ -11499,6 +11506,68 @@ function parseServerBindingResponse(raw) {
11499
11506
  }
11500
11507
 
11501
11508
  // src/bcudp/BcUdpStream.ts
11509
+ async function probeEgressForHost(destHost, destPort) {
11510
+ return await new Promise((resolve, reject) => {
11511
+ const probe = import_node_dgram.default.createSocket("udp4");
11512
+ let settled = false;
11513
+ const finish = (err, out) => {
11514
+ if (settled) return;
11515
+ settled = true;
11516
+ try {
11517
+ probe.close();
11518
+ } catch {
11519
+ }
11520
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
11521
+ else resolve(out);
11522
+ };
11523
+ probe.on("error", (e) => finish(e));
11524
+ try {
11525
+ probe.connect(destPort, destHost, () => {
11526
+ try {
11527
+ const a = probe.address();
11528
+ if (typeof a === "string") return finish(new Error("probe address is string"));
11529
+ finish(void 0, {
11530
+ localAddress: a.address,
11531
+ localPort: a.port
11532
+ });
11533
+ } catch (e) {
11534
+ finish(e);
11535
+ }
11536
+ });
11537
+ } catch (e) {
11538
+ finish(e);
11539
+ }
11540
+ });
11541
+ }
11542
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
11543
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
11544
+ const dest = destHost.split(".").map((s) => Number(s));
11545
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
11546
+ return "unknown";
11547
+ const ifaces = (0, import_node_os.networkInterfaces)();
11548
+ let ownerSubnet;
11549
+ for (const name of Object.keys(ifaces)) {
11550
+ const entries = ifaces[name];
11551
+ if (!entries) continue;
11552
+ for (const e of entries) {
11553
+ if (e.family !== "IPv4" || e.internal) continue;
11554
+ if (e.address !== srcInfo.localAddress) continue;
11555
+ const addr = e.address.split(".").map((s) => Number(s));
11556
+ const mask = e.netmask.split(".").map((s) => Number(s));
11557
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
11558
+ continue;
11559
+ ownerSubnet = { addr, mask };
11560
+ break;
11561
+ }
11562
+ if (ownerSubnet) break;
11563
+ }
11564
+ if (!ownerSubnet) return "unknown";
11565
+ for (let i = 0; i < 4; i++) {
11566
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
11567
+ return "mismatch";
11568
+ }
11569
+ return "same";
11570
+ }
11502
11571
  var AckLatency = class {
11503
11572
  currentValues = [];
11504
11573
  lastReceiveTime = null;
@@ -11577,6 +11646,11 @@ function isUnroutableForP2P(ip) {
11577
11646
  var P2P_LOOKUP_PORT = 9999;
11578
11647
  var P2P_MAX_WAIT_MS = 15e3;
11579
11648
  var P2P_RESEND_WAIT_MS = 500;
11649
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
11650
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
11651
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
11652
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
11653
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
11580
11654
  var BcUdpStream = class extends import_node_events3.EventEmitter {
11581
11655
  opts;
11582
11656
  /**
@@ -11665,31 +11739,14 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11665
11739
  });
11666
11740
  sock.on("error", (e) => this.emit("error", e));
11667
11741
  sock.on("close", () => this.emit("close"));
11668
- const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
11669
- for (let i = portRange.length - 1; i > 0; i--) {
11670
- const j = Math.floor(Math.random() * (i + 1));
11671
- [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
11672
- }
11673
- let bound = false;
11674
- for (const port of portRange) {
11675
- try {
11676
- await new Promise((resolve, reject) => {
11677
- sock.once("error", reject);
11678
- sock.bind(port, "0.0.0.0", () => {
11679
- sock.removeListener("error", reject);
11680
- resolve();
11681
- });
11682
- });
11683
- bound = true;
11684
- break;
11685
- } catch {
11686
- }
11687
- }
11688
- if (!bound) {
11689
- await new Promise(
11690
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
11691
- );
11692
- }
11742
+ await new Promise((resolve, reject) => {
11743
+ const onErr = (e) => reject(e);
11744
+ sock.once("error", onErr);
11745
+ sock.bind(0, "0.0.0.0", () => {
11746
+ sock.removeListener("error", onErr);
11747
+ resolve();
11748
+ });
11749
+ });
11693
11750
  if (this.opts.mode === "direct") {
11694
11751
  this.remote = { host: this.opts.host, port: this.opts.port };
11695
11752
  this.clientId = this.opts.clientId;
@@ -11760,6 +11817,49 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11760
11817
  this.remote = { host: connected.rhost, port: connected.rport };
11761
11818
  }
11762
11819
  async p2pUidLookup(sock, uid) {
11820
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11821
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11822
+ const cached = cachedP2pLookups.get(uid);
11823
+ if (cached && cached.expires > Date.now()) {
11824
+ log(
11825
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
11826
+ );
11827
+ return cached.result;
11828
+ }
11829
+ const negCached = negCachedP2pLookups.get(uid);
11830
+ if (negCached && negCached.expires > Date.now()) {
11831
+ const remaining = negCached.expires - Date.now();
11832
+ log(
11833
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
11834
+ );
11835
+ throw negCached.error;
11836
+ }
11837
+ const inflight = inflightP2pLookups.get(uid);
11838
+ if (inflight) {
11839
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
11840
+ return await inflight;
11841
+ }
11842
+ const work = this._doP2pUidLookupWork(sock, uid);
11843
+ inflightP2pLookups.set(uid, work);
11844
+ try {
11845
+ const result = await work;
11846
+ cachedP2pLookups.set(uid, {
11847
+ result,
11848
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
11849
+ });
11850
+ return result;
11851
+ } catch (e) {
11852
+ const err = e instanceof Error ? e : new Error(String(e));
11853
+ negCachedP2pLookups.set(uid, {
11854
+ error: err,
11855
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
11856
+ });
11857
+ throw err;
11858
+ } finally {
11859
+ inflightP2pLookups.delete(uid);
11860
+ }
11861
+ }
11862
+ async _doP2pUidLookupWork(sock, uid) {
11763
11863
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11764
11864
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11765
11865
  const t0 = Date.now();
@@ -12227,6 +12327,23 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
12227
12327
  log(
12228
12328
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
12229
12329
  );
12330
+ if (directHost && localMode === "local-direct") {
12331
+ try {
12332
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
12333
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
12334
+ if (sameSubnet === "mismatch") {
12335
+ log(
12336
+ `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.`
12337
+ );
12338
+ } else {
12339
+ log(
12340
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
12341
+ );
12342
+ }
12343
+ } catch (e) {
12344
+ this.emit("debug", "egress_probe_failed", e);
12345
+ }
12346
+ }
12230
12347
  let bytesSent = 0;
12231
12348
  let pktsRecv = 0;
12232
12349
  sock.on("message", () => {
@@ -31811,7 +31928,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
31811
31928
  return methods.filter((m) => m === "local-direct");
31812
31929
  }
31813
31930
  function normalizeUid(uid) {
31814
- const v = uid?.trim();
31931
+ const v = uid?.trim().toUpperCase();
31815
31932
  return v ? v : void 0;
31816
31933
  }
31817
31934
  function maskUid(uid) {