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