@decentnetwork/peer 0.1.64 → 0.1.66

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.
package/dist/peer.d.ts CHANGED
@@ -194,9 +194,6 @@ export declare class Peer {
194
194
  * tcp-relay, discovery itself is broken — we don't even know
195
195
  * where to send a cookie request. */
196
196
  endpointCandidatesCount: number;
197
- /** The actual candidate endpoints (host:port) — lets diag see whether a
198
- * same-LAN host candidate (e.g. 10.0.0.x) was received but not adopted. */
199
- endpointCandidates: string[];
200
197
  /** Last time we sent an outbound cookie request via UDP for this
201
198
  * peer (null = never). Used to confirm Phase 1.2 retries are
202
199
  * actually firing. */
package/dist/peer.js CHANGED
@@ -82,23 +82,6 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
82
82
  // slow enough not to burn CPU on idle peers. Tunable via env.
83
83
  const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
84
84
  const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
85
- // Half-open breaker: if the peer proposes a NEW session key this many times over
86
- // this long while our session looks "alive", accept it (the peer can't decrypt
87
- // our current session — a desynced half-open session, observed as a flood of
88
- // "no session matched"). Each handshake carries a fresh cookie so these are
89
- // genuine attempts, not replays. Bounded so it never fires on a healthy session.
90
- const REHS_ACCEPT_COUNT = readEnvInt("DECENT_REHS_ACCEPT_COUNT", 4);
91
- const REHS_ACCEPT_MS = readEnvInt("DECENT_REHS_ACCEPT_MS", 2500);
92
- const REHS_WINDOW_MS = readEnvInt("DECENT_REHS_WINDOW_MS", 8000);
93
- // Re-handshake when we keep receiving undecryptable crypto data from a known
94
- // friend's endpoint (desynced sessions), rate-limited per friend.
95
- // Conservative: deleting a session forces a re-handshake which ROTATES our
96
- // ephemeral key, so doing it too eagerly turns a transient blip into a
97
- // rotate→desync→delete thrash. Only nuke a session that's been UNABLE to
98
- // decrypt anything for a long time (genuinely dead, well past any
99
- // establishment-transition window), and at most once per long interval.
100
- const REINIT_ON_DESYNC_MS = readEnvInt("DECENT_REINIT_ON_DESYNC_MS", 12000);
101
- const REINIT_STUCK_MS = readEnvInt("DECENT_REINIT_STUCK_MS", 20000);
102
85
  // How long a relay path stays "confirmed" (carrying bulk data on its own, no
103
86
  // tcp-relay fan-out) after the last packet received over it. The relay
104
87
  // keepalive refreshes this every ~4s on a healthy path, so a generous window
@@ -273,9 +256,6 @@ export class Peer {
273
256
  // #initiateSession every few seconds for every friend; without this
274
257
  // dedupe, lower-pubkey side floods the log with "deferring to peer".
275
258
  #initiateSkipLogged = new Set();
276
- // Per-friend last time we deleted a desynced session (rate-limit; survives the
277
- // session deletion, unlike a field on the session object itself).
278
- #lastDesyncDeleteMs = new Map();
279
259
  // Cached server-reflexive (public) UDP endpoint of our #udp socket,
280
260
  // learned via STUN. Cone-NAT mappings are stable for the socket
281
261
  // lifetime, so we only re-probe every ~60s. Used to tell friends where
@@ -1082,7 +1062,6 @@ export class Peer {
1082
1062
  ourLocalUdpPort: this.#udp?.localPort() ?? null,
1083
1063
  friendUdpEndpoint: friendUdp,
1084
1064
  endpointCandidatesCount: s.endpointCandidates?.length ?? 0,
1085
- endpointCandidates: (s.endpointCandidates ?? []).map((c) => `${c.host}:${c.port}`),
1086
1065
  cookieRequestSentMs: s.cookieRequestSentMs ?? null,
1087
1066
  };
1088
1067
  }
@@ -1456,30 +1435,12 @@ export class Peer {
1456
1435
  const lastAlive = state.lastPingRecvMs ?? state.sessionEstablishedAtMs;
1457
1436
  const currentSessionAlive = Date.now() - lastAlive < FRIEND_TIMEOUT_MS;
1458
1437
  if (sinceEstablished > 1000 && currentSessionAlive) {
1459
- // Half-open breaker. Our session looks alive but "alive" only means
1460
- // WE are receiving (possibly only over the relay). If the peer KEEPS
1461
- // proposing a new key, THEIR side can't decrypt our packets: a desynced
1462
- // half-open session (observed snoopy↔mac on one LAN as 288k "no session
1463
- // matched"). Rejecting forever deadlocks. So count sustained attempts
1464
- // and, past the threshold, accept the new key to re-converge. Bounded
1465
- // to sustained attempts so it never disturbs a healthy session.
1466
- const now = Date.now();
1467
- if (state.rehsFirstMs === undefined || now - (state.rehsLastMs ?? 0) > REHS_WINDOW_MS) {
1468
- state.rehsFirstMs = now;
1469
- state.rehsCount = 0;
1470
- }
1471
- state.rehsLastMs = now;
1472
- state.rehsCount = (state.rehsCount ?? 0) + 1;
1473
- const stuck = state.rehsCount >= REHS_ACCEPT_COUNT && now - state.rehsFirstMs >= REHS_ACCEPT_MS;
1474
- if (!stuck) {
1475
- this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live session, attempt ${state.rehsCount}, ${sinceEstablished}ms after establish)`);
1476
- return;
1477
- }
1478
- this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (half-open breaker: ${state.rehsCount} new-key attempts over ${now - state.rehsFirstMs}ms — peer can't decrypt our session)`);
1438
+ // Verbose only peers retransmit handshakes aggressively, so
1439
+ // this line fires dozens of times per minute on a stuck pair
1440
+ // and drowns out signal. Visible with DECENT_DEBUG_VERBOSE=1.
1441
+ this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ${sinceEstablished}ms after establish)`);
1442
+ return;
1479
1443
  }
1480
- // Converging on a fresh session — clear the breaker counter.
1481
- state.rehsCount = 0;
1482
- state.rehsFirstMs = undefined;
1483
1444
  this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (current session wedged/dead, ` +
1484
1445
  `lastPingRecv=${state.lastPingRecvMs ? Date.now() - state.lastPingRecvMs + "ms ago" : "never"})`);
1485
1446
  }
@@ -1509,21 +1470,13 @@ export class Peer {
1509
1470
  this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
1510
1471
  if (!wasEstablished) {
1511
1472
  this.#debugLog(`friend_connected friend=${friendId} remote=${remote.address}:${remote.port}`);
1512
- // Send our UDP endpoint offer (public srflx + LAN host candidate)
1513
- // whenever the current path isn't already a same-LAN direct one:
1514
- // - relay-only sessions need it to bootstrap a direct UDP path; and
1515
- // - sessions that came up over a PUBLIC UDP endpoint (internet
1516
- // hairpin) when both peers are actually on the SAME LAN — without
1517
- // the offer they never learn each other's 10.x/192.168.x address and
1518
- // stay stuck on the public path, which a symmetric / hairpin NAT
1519
- // breaks one-way (observed: two boxes on one LAN where each only saw
1520
- // the other one-directionally). The peer's same-subnet filter drops
1521
- // the LAN candidate when it doesn't apply, so offering is always safe.
1522
- const r = state.remote;
1523
- const remoteIsSameLan = !!r && isPrivateAddress(r.host) && !isCgnatAddress(r.host) &&
1524
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(r.host, s));
1525
- const haveLanToOffer = getPhysicalLanAddresses().some((ip) => isPrivateAddress(ip) && !isCgnatAddress(ip));
1526
- if ((state.hasTcpRoute && !r) || (!remoteIsSameLan && haveLanToOffer)) {
1473
+ // If we only have a TCP-relay path, immediately tell the peer our
1474
+ // public UDP endpoint so they can hole-punch — don't wait for the
1475
+ // 15s UDP-retry loop, which can miss a flapping relay session's
1476
+ // short-lived establishment window. Both sides do this on every
1477
+ // (re)establishment, so a brief tcp-relay window is enough to
1478
+ // bootstrap the direct UDP path. See docs/UDP-DIRECT-PLAN.md.
1479
+ if (state.hasTcpRoute && !state.remote) {
1527
1480
  void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
1528
1481
  }
1529
1482
  }
@@ -1645,50 +1598,34 @@ export class Peer {
1645
1598
  innerKind === PACKET_ID_FILE_SENDREQUEST ||
1646
1599
  innerKind === PACKET_ID_FILE_CONTROL ||
1647
1600
  innerKind === PACKET_ID_FILE_DATA;
1648
- // Confirm the transport path + liveness for EVERY decrypted packet —
1649
- // INCLUDING a dual-send duplicate. Receiving over this transport proves
1650
- // it works, so this MUST run BEFORE the dedup drop below. Otherwise the
1651
- // duplicate UDP copy (the relay copy already advanced the buffer) is
1652
- // dropped without ever marking the direct-UDP path live → lastUdpRecvMs
1653
- // stays unset → udpFresh false → bulk data (files) never ride the LAN
1654
- // even when LAN UDP is flowing both ways (the snoopy slow-file bug).
1601
+ if (!isSingleTransportKind && opened.packetNumber < (state.receiveBufferStart ?? 0)) {
1602
+ continue;
1603
+ }
1604
+ state.peerBaseNonce[state.peerBaseNonce.length - 2] = packet[1];
1605
+ state.peerBaseNonce[state.peerBaseNonce.length - 1] = packet[2];
1606
+ incrementNonce(state.peerBaseNonce);
1655
1607
  state.lastPingRecvMs = Date.now();
1608
+ // Same TCP-vs-UDP rule as the handshake handlers: don't poison
1609
+ // the UDP send-back endpoint when this packet came in via TCP.
1656
1610
  if (this.#remoteIsTcp(remote)) {
1657
1611
  state.hasTcpRoute = true;
1658
1612
  }
1659
1613
  else if (viaRelay) {
1614
+ // Arrived over the TURN relay. Track relay freshness separately;
1615
+ // do NOT set state.remote (that's the direct endpoint). The relay
1616
+ // address it came from is already in state.relayRemote.
1660
1617
  if (!state.lastRelayRecvMs) {
1661
1618
  this.#debugLog(`relay_confirmed friend=${friendId} via=${remote.address}:${remote.port} (TURN relay path live)`);
1662
1619
  }
1663
1620
  state.lastRelayRecvMs = Date.now();
1664
1621
  }
1665
1622
  else {
1666
- // Adopt the source as our send endpoint — but NEVER downgrade from a
1667
- // same-LAN (physical) path to a non-LAN source (a hairpin packet via
1668
- // the peer's public / Tailscale-exit endpoint must not knock us off
1669
- // the direct LAN path, which is strictly better).
1670
- const cur = state.remote?.host;
1671
- const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
1672
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
1673
- const incomingSameLan = isPrivateAddress(remote.address) && !isCgnatAddress(remote.address) &&
1674
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s));
1675
- if (incomingSameLan || !onSameLanAlready) {
1676
- state.remote = { host: remote.address, port: remote.port };
1677
- }
1623
+ state.remote = { host: remote.address, port: remote.port };
1678
1624
  if (!state.lastUdpRecvMs) {
1679
1625
  this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
1680
1626
  }
1681
1627
  state.lastUdpRecvMs = Date.now();
1682
1628
  }
1683
- // De-duplicate: skip the DISPATCH + nonce advance for an already-seen
1684
- // packet number. The transport confirmation above already ran, so a
1685
- // duplicate still keeps the path fresh.
1686
- if (!isSingleTransportKind && opened.packetNumber < (state.receiveBufferStart ?? 0)) {
1687
- continue;
1688
- }
1689
- state.peerBaseNonce[state.peerBaseNonce.length - 2] = packet[1];
1690
- state.peerBaseNonce[state.peerBaseNonce.length - 1] = packet[2];
1691
- incrementNonce(state.peerBaseNonce);
1692
1629
  state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
1693
1630
  const kind = opened.payload[0];
1694
1631
  const inner = opened.payload.slice(1);
@@ -1841,44 +1778,7 @@ export class Peer {
1841
1778
  this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
1842
1779
  return;
1843
1780
  }
1844
- // We received crypto data we can't decrypt from a KNOWN friend: our two
1845
- // sessions are desynced (different keys) and both are "established", so
1846
- // neither re-handshakes (#initiateSession no-ops on an established session,
1847
- // and the lower-pubkey side defers entirely). DELETE the stale session so
1848
- // the connection loop re-establishes cleanly via the single-initiator path
1849
- // (higher pubkey initiates a fresh cookie chain, lower responds, both
1850
- // derive the SAME key). This covers BOTH transports — the desync shows up
1851
- // over the TCP relay (remote = tcp:<friendId>:0) as well as direct UDP.
1852
- let desyncedFid;
1853
- if (this.#remoteIsTcp(remote)) {
1854
- const m = /^tcp:([A-Za-z0-9]+)/.exec(remote.address);
1855
- if (m && this.#friendSessions.has(m[1]))
1856
- desyncedFid = m[1];
1857
- }
1858
- else if (!viaRelay) {
1859
- for (const [fid, st] of this.#friendSessions.entries()) {
1860
- if ((st.remote?.host === remote.address && st.remote?.port === remote.port) ||
1861
- (st.endpointCandidates ?? []).some((c) => c.host === remote.address && c.port === remote.port)) {
1862
- desyncedFid = fid;
1863
- break;
1864
- }
1865
- }
1866
- }
1867
- if (desyncedFid) {
1868
- const st = this.#friendSessions.get(desyncedFid);
1869
- // Only a GENUINELY stuck session (no successful decrypt for a while) —
1870
- // never nuke a healthy one on a stray/late undecryptable packet — and
1871
- // rate-limited via a map that survives the delete.
1872
- const working = st.lastPingRecvMs !== undefined && Date.now() - st.lastPingRecvMs < REINIT_STUCK_MS;
1873
- const lastDel = this.#lastDesyncDeleteMs.get(desyncedFid) ?? 0;
1874
- if (!working && Date.now() - lastDel > REINIT_ON_DESYNC_MS) {
1875
- this.#lastDesyncDeleteMs.set(desyncedFid, Date.now());
1876
- this.#debugLog(`no-session-matched from ${remote.address} (friend ${desyncedFid}) — desynced, deleting session for clean re-handshake`);
1877
- this.#friendSessions.delete(desyncedFid);
1878
- void this.#initiateSession(desyncedFid).catch(() => undefined);
1879
- }
1880
- }
1881
- this.#debugVerboseLog(`crypto data received from ${remote.address}:${remote.port} but no session matched`);
1781
+ this.#debugLog(`crypto data received from ${remote.address}:${remote.port} but no session matched`);
1882
1782
  return;
1883
1783
  }
1884
1784
  if (packet[0] === NET_PACKET_ONION_DATA_RESPONSE && this.#announceDataKey) {
@@ -3277,13 +3177,11 @@ export class Peer {
3277
3177
  resolve();
3278
3178
  });
3279
3179
  });
3280
- // CRITICAL: a persistent 'error' listener. A TURN send to an
3281
- // unresolvable host (DNS NXDOMAIN e.g. tokyo.fi.chat on a box whose
3282
- // resolver doesn't know it) emits an 'error' on this dgram socket;
3283
- // with no listener, Node re-throws it as an uncaught exception and
3284
- // CRASHES THE ENTIRE DAEMON (observed: snoopy repeatedly dying on
3285
- // "getaddrinfo ENOTFOUND tokyo.fi.chat", which churned every session).
3286
- // Swallow it — the relay simply won't allocate/relay over this server.
3180
+ // CRITICAL: persistent 'error' listener. A TURN send to an unresolvable
3181
+ // host (transient DNS NXDOMAIN, e.g. tokyo.fi.chat) emits an 'error' on
3182
+ // this dgram socket; with no listener Node re-throws it as an uncaught
3183
+ // exception and CRASHES THE WHOLE DAEMON (every session dropped, mass
3184
+ // re-handshake churn). Swallow it the relay just won't allocate here.
3287
3185
  sock.on("error", (e) => this.#debugLog(`turn socket error (${srv.host}): ${e.message}`));
3288
3186
  const client = new TurnClient({
3289
3187
  sock,
@@ -3453,19 +3351,8 @@ export class Peer {
3453
3351
  if (++ln >= 6)
3454
3352
  clearInterval(lanTimer);
3455
3353
  }, 120);
3456
- // The physical LAN is strictly better than ANY public path — direct, no
3457
- // NAT, no relay, and crucially no Tailscale-exit hairpin (a peer whose
3458
- // internet egresses via Tailscale advertises its exit's public IP as
3459
- // its srflx, so the "direct" public path actually loops out through
3460
- // Tailscale and back). So adopt the LAN candidate as session.remote even
3461
- // when we already have a public "real UDP" remote — UNLESS we're already
3462
- // on a same-LAN address (don't flap between two LAN candidates). Without
3463
- // this the session stuck to the public/Tailscale endpoint and never sent
3464
- // over the LAN (observed: session.remote stayed public, data on relay).
3465
- const cur = session.remote?.host;
3466
- const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
3467
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
3468
- if (!onSameLanAlready) {
3354
+ const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
3355
+ if (!haveRealUdp) {
3469
3356
  session.remote = { host: lanHost, port: lanPort };
3470
3357
  }
3471
3358
  if (session.established) {
@@ -3502,14 +3389,7 @@ export class Peer {
3502
3389
  // upgrading the path to direct UDP in both directions with no new
3503
3390
  // handshake. If UDP never works, the periodic offer re-punches.
3504
3391
  const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
3505
- // NEVER point the session at the public srflx when we know a same-LAN
3506
- // candidate for this peer: the physical LAN is strictly better, and letting
3507
- // the srflx win here caused a LAN↔public FLAP (the LAN block above set
3508
- // remote=LAN, then this overwrote it with the Tailscale-exit public IP),
3509
- // so the direct LAN path never stabilised and udpRecv never confirmed.
3510
- const haveLanCandidate = (session.endpointCandidates ?? []).some((c) => isPrivateAddress(c.host) && !isCgnatAddress(c.host) &&
3511
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(c.host, s)));
3512
- if (!haveRealUdp && !haveLanCandidate) {
3392
+ if (!haveRealUdp) {
3513
3393
  session.remote = { host, port };
3514
3394
  }
3515
3395
  // Nudge a keepalive out immediately so the upgrade doesn't wait for
@@ -4087,39 +3967,6 @@ export class Peer {
4087
3967
  if (!node.isTcp)
4088
3968
  void this.#sendDhtPing(node);
4089
3969
  }
4090
- // 4. LAN-direct upgrade. Re-offer our endpoint (public srflx + LAN host
4091
- // candidate) to any established friend whose current path is NOT already
4092
- // a same-LAN address, as long as we have a LAN address to advertise. This
4093
- // upgrades sessions that came up over a public UDP endpoint (internet
4094
- // hairpin) to the direct LAN path even without a re-establish, and self-
4095
- // stops once the path becomes same-LAN. The peer's same-subnet filter
4096
- // drops the candidate when it doesn't apply, so this is always safe.
4097
- const haveLan = getPhysicalLanAddresses().some((ip) => isPrivateAddress(ip) && !isCgnatAddress(ip));
4098
- if (haveLan) {
4099
- for (const [friendId, session] of this.#friendSessions.entries()) {
4100
- if (!session.established)
4101
- continue;
4102
- const r = session.remote;
4103
- const remoteIsSameLan = !!r && isPrivateAddress(r.host) && !isCgnatAddress(r.host) &&
4104
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(r.host, s));
4105
- if (!remoteIsSameLan) {
4106
- // If we ALREADY hold a same-LAN candidate for this peer (learned from
4107
- // an earlier offer), switch the session onto it NOW — don't wait to
4108
- // receive yet another offer at exactly the right moment. That delay is
4109
- // what kept sessions stuck on the public/Tailscale-hairpin path after
4110
- // the staggered restarts. Then send a keepalive over the LAN so the
4111
- // peer learns our LAN source and replies over it; the downgrade guard
4112
- // keeps us there once it sticks.
4113
- const lanCand = (session.endpointCandidates ?? []).find((c) => isPrivateAddress(c.host) && !isCgnatAddress(c.host) &&
4114
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(c.host, s)));
4115
- if (lanCand) {
4116
- session.remote = { host: lanCand.host, port: lanCand.port };
4117
- void this.#sendMessengerPacket(friendId, PACKET_ID_ALIVE, new Uint8Array()).catch(() => undefined);
4118
- }
4119
- void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
4120
- }
4121
- }
4122
- }
4123
3970
  }
4124
3971
  /**
4125
3972
  * Send an onion DHT-PK announcement to a friend so they learn our DHT
@@ -5052,15 +4899,7 @@ function isInIpv4Subnet(host, subnet) {
5052
4899
  // path onto the overlay or loops into our own mesh, then silently falls back to
5053
4900
  // the relay — the snoopy/lili "lastUdpRecv=never -> bufferbloat" path. (lili
5054
4901
  // runs Tailscale; that's exactly this.)
5055
- // Names of interfaces that are NOT a real physical LAN — overlays (tailscale,
5056
- // zerotier, wireguard, the agentnet TUN), and especially container/VM bridges.
5057
- // Docker creates `docker0` AND per-network `br-<hash>` bridges (172.x); libvirt
5058
- // `virbr0`; k8s `cni0`/`flannel`/`kube`. These were leaking their 172.x address
5059
- // into the LAN host-candidate offer, so a peer on a real 10.x LAN advertised a
5060
- // Docker-bridge IP its LAN partner could never match → LAN-direct never engaged
5061
- // (observed: two boxes on one 10.0.0.0/24 stuck hairpinning through the public
5062
- // internet). `^bridge` did NOT cover `br-…`.
5063
- const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|br-|virbr|cni|flannel|weave|kube|vnet|vnic|vmnet|veth|docker)/i;
4902
+ const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|vnic|vmnet|veth|docker)/i;
5064
4903
  /** True for RFC 6598 carrier-grade-NAT space 100.64.0.0/10 (Tailscale's range). */
5065
4904
  function isCgnatAddress(host) {
5066
4905
  const o = host.split(".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",