@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.
@@ -689,6 +689,7 @@ function readCache(uid, now) {
689
689
  }
690
690
  async function getServerBinding(uid, options = {}) {
691
691
  if (!uid || typeof uid !== "string") return void 0;
692
+ uid = uid.toUpperCase();
692
693
  const now = Date.now();
693
694
  const cached = readCache(uid, now);
694
695
  if (cached?.kind === "ok") return cached.response;
@@ -731,8 +732,14 @@ async function getServerBinding(uid, options = {}) {
731
732
  headers: { Accept: "application/json" }
732
733
  });
733
734
  if (!res.ok) {
735
+ let bodyPreview;
736
+ try {
737
+ const text = await res.text();
738
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
739
+ } catch {
740
+ }
734
741
  logger?.log?.(
735
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
742
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
736
743
  );
737
744
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
738
745
  return void 0;
@@ -848,6 +855,68 @@ function parseServerBindingResponse(raw) {
848
855
  }
849
856
 
850
857
  // src/bcudp/BcUdpStream.ts
858
+ async function probeEgressForHost(destHost, destPort) {
859
+ return await new Promise((resolve, reject) => {
860
+ const probe = dgram.createSocket("udp4");
861
+ let settled = false;
862
+ const finish = (err, out) => {
863
+ if (settled) return;
864
+ settled = true;
865
+ try {
866
+ probe.close();
867
+ } catch {
868
+ }
869
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
870
+ else resolve(out);
871
+ };
872
+ probe.on("error", (e) => finish(e));
873
+ try {
874
+ probe.connect(destPort, destHost, () => {
875
+ try {
876
+ const a = probe.address();
877
+ if (typeof a === "string") return finish(new Error("probe address is string"));
878
+ finish(void 0, {
879
+ localAddress: a.address,
880
+ localPort: a.port
881
+ });
882
+ } catch (e) {
883
+ finish(e);
884
+ }
885
+ });
886
+ } catch (e) {
887
+ finish(e);
888
+ }
889
+ });
890
+ }
891
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
892
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
893
+ const dest = destHost.split(".").map((s) => Number(s));
894
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
895
+ return "unknown";
896
+ const ifaces = networkInterfaces();
897
+ let ownerSubnet;
898
+ for (const name of Object.keys(ifaces)) {
899
+ const entries = ifaces[name];
900
+ if (!entries) continue;
901
+ for (const e of entries) {
902
+ if (e.family !== "IPv4" || e.internal) continue;
903
+ if (e.address !== srcInfo.localAddress) continue;
904
+ const addr = e.address.split(".").map((s) => Number(s));
905
+ const mask = e.netmask.split(".").map((s) => Number(s));
906
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
907
+ continue;
908
+ ownerSubnet = { addr, mask };
909
+ break;
910
+ }
911
+ if (ownerSubnet) break;
912
+ }
913
+ if (!ownerSubnet) return "unknown";
914
+ for (let i = 0; i < 4; i++) {
915
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
916
+ return "mismatch";
917
+ }
918
+ return "same";
919
+ }
851
920
  var AckLatency = class {
852
921
  currentValues = [];
853
922
  lastReceiveTime = null;
@@ -926,6 +995,16 @@ function isUnroutableForP2P(ip) {
926
995
  var P2P_LOOKUP_PORT = 9999;
927
996
  var P2P_MAX_WAIT_MS = 15e3;
928
997
  var P2P_RESEND_WAIT_MS = 500;
998
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
999
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
1000
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
1001
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
1002
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
1003
+ function _clearP2pLookupDedupForTests() {
1004
+ inflightP2pLookups.clear();
1005
+ cachedP2pLookups.clear();
1006
+ negCachedP2pLookups.clear();
1007
+ }
929
1008
  var BcUdpStream = class extends EventEmitter {
930
1009
  opts;
931
1010
  /**
@@ -1014,31 +1093,14 @@ var BcUdpStream = class extends EventEmitter {
1014
1093
  });
1015
1094
  sock.on("error", (e) => this.emit("error", e));
1016
1095
  sock.on("close", () => this.emit("close"));
1017
- const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
1018
- for (let i = portRange.length - 1; i > 0; i--) {
1019
- const j = Math.floor(Math.random() * (i + 1));
1020
- [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
1021
- }
1022
- let bound = false;
1023
- for (const port of portRange) {
1024
- try {
1025
- await new Promise((resolve, reject) => {
1026
- sock.once("error", reject);
1027
- sock.bind(port, "0.0.0.0", () => {
1028
- sock.removeListener("error", reject);
1029
- resolve();
1030
- });
1031
- });
1032
- bound = true;
1033
- break;
1034
- } catch {
1035
- }
1036
- }
1037
- if (!bound) {
1038
- await new Promise(
1039
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
1040
- );
1041
- }
1096
+ await new Promise((resolve, reject) => {
1097
+ const onErr = (e) => reject(e);
1098
+ sock.once("error", onErr);
1099
+ sock.bind(0, "0.0.0.0", () => {
1100
+ sock.removeListener("error", onErr);
1101
+ resolve();
1102
+ });
1103
+ });
1042
1104
  if (this.opts.mode === "direct") {
1043
1105
  this.remote = { host: this.opts.host, port: this.opts.port };
1044
1106
  this.clientId = this.opts.clientId;
@@ -1109,6 +1171,49 @@ var BcUdpStream = class extends EventEmitter {
1109
1171
  this.remote = { host: connected.rhost, port: connected.rport };
1110
1172
  }
1111
1173
  async p2pUidLookup(sock, uid) {
1174
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
1175
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
1176
+ const cached = cachedP2pLookups.get(uid);
1177
+ if (cached && cached.expires > Date.now()) {
1178
+ log(
1179
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
1180
+ );
1181
+ return cached.result;
1182
+ }
1183
+ const negCached = negCachedP2pLookups.get(uid);
1184
+ if (negCached && negCached.expires > Date.now()) {
1185
+ const remaining = negCached.expires - Date.now();
1186
+ log(
1187
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
1188
+ );
1189
+ throw negCached.error;
1190
+ }
1191
+ const inflight = inflightP2pLookups.get(uid);
1192
+ if (inflight) {
1193
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
1194
+ return await inflight;
1195
+ }
1196
+ const work = this._doP2pUidLookupWork(sock, uid);
1197
+ inflightP2pLookups.set(uid, work);
1198
+ try {
1199
+ const result = await work;
1200
+ cachedP2pLookups.set(uid, {
1201
+ result,
1202
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
1203
+ });
1204
+ return result;
1205
+ } catch (e) {
1206
+ const err = e instanceof Error ? e : new Error(String(e));
1207
+ negCachedP2pLookups.set(uid, {
1208
+ error: err,
1209
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
1210
+ });
1211
+ throw err;
1212
+ } finally {
1213
+ inflightP2pLookups.delete(uid);
1214
+ }
1215
+ }
1216
+ async _doP2pUidLookupWork(sock, uid) {
1112
1217
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
1113
1218
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
1114
1219
  const t0 = Date.now();
@@ -1576,6 +1681,23 @@ var BcUdpStream = class extends EventEmitter {
1576
1681
  log(
1577
1682
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
1578
1683
  );
1684
+ if (directHost && localMode === "local-direct") {
1685
+ try {
1686
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
1687
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
1688
+ if (sameSubnet === "mismatch") {
1689
+ log(
1690
+ `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.`
1691
+ );
1692
+ } else {
1693
+ log(
1694
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
1695
+ );
1696
+ }
1697
+ } catch (e) {
1698
+ this.emit("debug", "egress_probe_failed", e);
1699
+ }
1700
+ }
1579
1701
  let bytesSent = 0;
1580
1702
  let pktsRecv = 0;
1581
1703
  sock.on("message", () => {
@@ -24583,7 +24705,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
24583
24705
  return methods.filter((m) => m === "local-direct");
24584
24706
  }
24585
24707
  function normalizeUid(uid) {
24586
- const v = uid?.trim();
24708
+ const v = uid?.trim().toUpperCase();
24587
24709
  return v ? v : void 0;
24588
24710
  }
24589
24711
  function maskUid(uid) {
@@ -25304,7 +25426,10 @@ export {
25304
25426
  encodeHeader,
25305
25427
  decodeHeader,
25306
25428
  BaichuanFrameParser,
25429
+ probeEgressForHost,
25430
+ isSameSubnetAsAnyLocalIface,
25307
25431
  isUnroutableForP2P,
25432
+ _clearP2pLookupDedupForTests,
25308
25433
  BcUdpStream,
25309
25434
  asLogger,
25310
25435
  createNullLogger,
@@ -25372,4 +25497,4 @@ export {
25372
25497
  tcpReachabilityProbe,
25373
25498
  autoDetectDeviceType
25374
25499
  };
25375
- //# sourceMappingURL=chunk-D4TKRGUP.js.map
25500
+ //# sourceMappingURL=chunk-UL34MR4L.js.map