@decentnetwork/peer 0.1.90 → 0.1.92
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.js +81 -34
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -174,7 +174,7 @@ const AGENTNET_PROTO_VERSION = 1;
|
|
|
174
174
|
// Peer package version, advertised as the default appVersion when the embedder
|
|
175
175
|
// doesn't override it. Read lazily so a bundler that inlines this file doesn't
|
|
176
176
|
// need the package.json at runtime.
|
|
177
|
-
const PEER_PKG_VERSION = "0.1.
|
|
177
|
+
const PEER_PKG_VERSION = "0.1.91";
|
|
178
178
|
// Toxcore Messenger.h packet IDs (live inside encrypted 0x1b crypto data plain payload)
|
|
179
179
|
const PACKET_ID_PADDING = 0;
|
|
180
180
|
const PACKET_ID_REQUEST = 1; // request retransmission of unreceived packets
|
|
@@ -1725,7 +1725,45 @@ export class Peer {
|
|
|
1725
1725
|
else {
|
|
1726
1726
|
this.#adoptRemote(state, remote.address, remote.port);
|
|
1727
1727
|
}
|
|
1728
|
-
|
|
1728
|
+
// CONVERGENCE RESYNC: the peer is retransmitting the SAME session pubkey
|
|
1729
|
+
// we already hold, so OUR view of the session looks fine. But if we've
|
|
1730
|
+
// been receiving crypto data from this peer we CANNOT decrypt, the peer
|
|
1731
|
+
// is holding a STALE copy of OUR session pubkey (it re-keyed, or we did,
|
|
1732
|
+
// and our reply handshake never reached it) — it encrypts to a key we no
|
|
1733
|
+
// longer have, so every data packet is "no session matched" and it keeps
|
|
1734
|
+
// retransmitting its handshake forever. Silently preserving deadlocks the
|
|
1735
|
+
// session PERMANENTLY (observed callpass↔mac: 11h, 20k undecryptable, IP
|
|
1736
|
+
// + messages both dead while presence shows "online"). Re-send our reply
|
|
1737
|
+
// handshake so the peer re-derives its shared key against our CURRENT
|
|
1738
|
+
// session pubkey. Rate-limited (peer retransmits ~1/s); a healthy session
|
|
1739
|
+
// never sets undecryptableRecvMs, so this can NEVER churn a working one.
|
|
1740
|
+
const now = Date.now();
|
|
1741
|
+
const peerSendingUndecryptable = state.undecryptableRecvMs !== undefined &&
|
|
1742
|
+
now - state.undecryptableRecvMs < FRIEND_TIMEOUT_MS;
|
|
1743
|
+
const resyncDue = now - (state.lastResyncHandshakeMs ?? 0) > 2000;
|
|
1744
|
+
if (peerSendingUndecryptable && resyncDue) {
|
|
1745
|
+
state.lastResyncHandshakeMs = now;
|
|
1746
|
+
state.lastPingRecvMs = now;
|
|
1747
|
+
const reply = createCryptoHandshake({
|
|
1748
|
+
recipientCookie: hs.embeddedCookie,
|
|
1749
|
+
baseNonce: state.ourBaseNonce,
|
|
1750
|
+
sessionPublicKey: state.ourSessionKeyPair.publicKey,
|
|
1751
|
+
senderRealSecretKey: this.#keyPair.secretKey,
|
|
1752
|
+
senderRealPublicKey: this.#keyPair.publicKey,
|
|
1753
|
+
senderDhtPublicKey: this.#keyPair.publicKey,
|
|
1754
|
+
receiverRealPublicKey: hs.senderRealPublicKey,
|
|
1755
|
+
receiverDhtPublicKey: hs.senderDhtPublicKey,
|
|
1756
|
+
localCookieSymmetricKey: this.#cookieSymmetricKey
|
|
1757
|
+
});
|
|
1758
|
+
const sendResync = () => this.#remoteIsTcp(remote)
|
|
1759
|
+
? Promise.resolve(this.#tcpRelays?.sendOobToFriend(hs.senderDhtPublicKey, reply) ?? 0)
|
|
1760
|
+
: this.#sendPacket(reply, { host: remote.address, port: remote.port });
|
|
1761
|
+
void sendResync()
|
|
1762
|
+
.then(() => this.#debugLog(`hs_recv duplicate but peer sent undecryptable data — re-sent our handshake to resync friend=${friendId}`))
|
|
1763
|
+
.catch((error) => this.#debugLog(`resync handshake failed for ${friendId}: ${error.message}`));
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
state.lastPingRecvMs = now;
|
|
1729
1767
|
this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
|
|
1730
1768
|
return;
|
|
1731
1769
|
}
|
|
@@ -2033,23 +2071,25 @@ export class Peer {
|
|
|
2033
2071
|
// packet number. The transport confirmation above already ran, so a
|
|
2034
2072
|
// duplicate still keeps the path fresh.
|
|
2035
2073
|
//
|
|
2036
|
-
// EXCEPTION:
|
|
2037
|
-
//
|
|
2074
|
+
// EXCEPTION: multi-fragment reliable messages that reassemble by tid —
|
|
2075
|
+
// BULKMSG (inline files) and INVITE_REQUEST/RESPONSE (WebRTC call SDPs) —
|
|
2076
|
+
// can arrive REORDERED. The C SDK puts totalsz on the FIRST fragment, but
|
|
2038
2077
|
// over a dual/UDP path a later fragment can land first, advancing the
|
|
2039
|
-
// low-water past the first fragment's number. Dropping the first
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2078
|
+
// low-water past the first fragment's number. Dropping the first fragment
|
|
2079
|
+
// here makes every fragment read totalsz 0 → the message is lost or (for
|
|
2080
|
+
// an SDP) scrambled, so a call gets stuck "connecting" and files vanish
|
|
2081
|
+
// ("bulkmsg totalsz 0 — dropped"). Let those through; #assembleBulkMsg /
|
|
2082
|
+
// #assembleInvite dedup AND order fragments by packet number.
|
|
2044
2083
|
if (!isSingleTransportKind && opened.packetNumber < (state.receiveBufferStart ?? 0)) {
|
|
2045
|
-
let
|
|
2084
|
+
let reorderable = false;
|
|
2046
2085
|
if (innerKind === PACKET_ID_MESSAGE) {
|
|
2047
2086
|
try {
|
|
2048
|
-
|
|
2087
|
+
const t = decodeCarrierPacket(opened.payload.slice(1)).type;
|
|
2088
|
+
reorderable = t === PACKET_TYPE_BULKMSG || t === PACKET_TYPE_INVITE_REQUEST || t === PACKET_TYPE_INVITE_RESPONSE;
|
|
2049
2089
|
}
|
|
2050
2090
|
catch { /* not a carrier packet */ }
|
|
2051
2091
|
}
|
|
2052
|
-
if (!
|
|
2092
|
+
if (!reorderable)
|
|
2053
2093
|
continue;
|
|
2054
2094
|
}
|
|
2055
2095
|
// Track the receive nonce from the EXACT nonce this packet used
|
|
@@ -3346,7 +3386,7 @@ export class Peer {
|
|
|
3346
3386
|
// signals (SDP offers) fragment by tid like bulkmsg; reassemble then
|
|
3347
3387
|
// surface as an "invite" event (NOT chat text).
|
|
3348
3388
|
if (carrier?.type === PACKET_TYPE_INVITE_REQUEST) {
|
|
3349
|
-
const complete = this.#assembleInvite(friendId, carrier);
|
|
3389
|
+
const complete = this.#assembleInvite(friendId, carrier, packetNumber);
|
|
3350
3390
|
if (!complete)
|
|
3351
3391
|
return; // more fragments pending
|
|
3352
3392
|
this.#events.emit("invite", {
|
|
@@ -3359,7 +3399,7 @@ export class Peer {
|
|
|
3359
3399
|
return;
|
|
3360
3400
|
}
|
|
3361
3401
|
if (carrier?.type === PACKET_TYPE_INVITE_RESPONSE) {
|
|
3362
|
-
const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier) : new Uint8Array();
|
|
3402
|
+
const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier, packetNumber) : new Uint8Array();
|
|
3363
3403
|
if (carrier.status === 0 && !complete)
|
|
3364
3404
|
return; // more fragments pending
|
|
3365
3405
|
this.#events.emit("inviteResponse", {
|
|
@@ -3556,12 +3596,14 @@ export class Peer {
|
|
|
3556
3596
|
const nums = [...entry.frags.keys()].sort((a, b) => a - b);
|
|
3557
3597
|
return concatBytes(nums.map((n) => entry.frags.get(n)));
|
|
3558
3598
|
}
|
|
3559
|
-
/** Append a Carrier invite fragment (INVITE_REQUEST/RESPONSE)
|
|
3560
|
-
* full reassembled payload
|
|
3561
|
-
*
|
|
3562
|
-
*
|
|
3563
|
-
*
|
|
3564
|
-
|
|
3599
|
+
/** Append a Carrier invite fragment (INVITE_REQUEST/RESPONSE) and return the
|
|
3600
|
+
* full reassembled payload once every fragment has landed. Like
|
|
3601
|
+
* #assembleBulkMsg, fragments are keyed + ordered by PACKET NUMBER so a
|
|
3602
|
+
* reordered multi-fragment WebRTC SDP (offer/answer >1280 bytes) reassembles
|
|
3603
|
+
* correctly instead of scrambling — a scrambled SDP left calls stuck
|
|
3604
|
+
* "connecting". Caps at CARRIER_MAX_INVITE_DATA_LEN (8192). Single-fragment
|
|
3605
|
+
* invites (the common case) complete on the first call. */
|
|
3606
|
+
#assembleInvite(friendId, frag, packetNumber) {
|
|
3565
3607
|
const now = Date.now();
|
|
3566
3608
|
for (const [key, entry] of this.#inviteAssembly) {
|
|
3567
3609
|
if (entry.expireAtMs < now)
|
|
@@ -3570,26 +3612,31 @@ export class Peer {
|
|
|
3570
3612
|
const key = `${friendId}:${frag.tid.toString()}`;
|
|
3571
3613
|
let entry = this.#inviteAssembly.get(key);
|
|
3572
3614
|
if (!entry) {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3615
|
+
entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + 60_000 };
|
|
3616
|
+
this.#inviteAssembly.set(key, entry);
|
|
3617
|
+
}
|
|
3618
|
+
// The first fragment carries totalsz; it may arrive out of order.
|
|
3619
|
+
if (frag.totalsz > 0) {
|
|
3620
|
+
if (frag.totalsz > CARRIER_MAX_INVITE_DATA_LEN) {
|
|
3621
|
+
this.#debugLog(`invite from ${friendId} totalsz ${frag.totalsz} exceeds cap — dropped`);
|
|
3622
|
+
this.#inviteAssembly.delete(key);
|
|
3577
3623
|
return undefined;
|
|
3578
3624
|
}
|
|
3579
|
-
entry =
|
|
3580
|
-
this.#inviteAssembly.set(key, entry);
|
|
3625
|
+
entry.total = frag.totalsz;
|
|
3581
3626
|
}
|
|
3582
|
-
if (frag.data.length
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
return undefined;
|
|
3627
|
+
if (frag.data.length > 0 && !entry.frags.has(packetNumber)) {
|
|
3628
|
+
entry.frags.set(packetNumber, frag.data);
|
|
3629
|
+
entry.got += frag.data.length;
|
|
3586
3630
|
}
|
|
3587
|
-
entry.
|
|
3588
|
-
entry.got += frag.data.length;
|
|
3589
|
-
if (entry.got < entry.total)
|
|
3631
|
+
if (entry.total === 0 || entry.got < entry.total)
|
|
3590
3632
|
return undefined;
|
|
3591
3633
|
this.#inviteAssembly.delete(key);
|
|
3592
|
-
|
|
3634
|
+
if (entry.got > entry.total) {
|
|
3635
|
+
this.#debugLog(`invite from ${friendId} got ${entry.got} > total ${entry.total} — dropped`);
|
|
3636
|
+
return undefined;
|
|
3637
|
+
}
|
|
3638
|
+
const nums = [...entry.frags.keys()].sort((a, b) => a - b);
|
|
3639
|
+
return concatBytes(nums.map((n) => entry.frags.get(n)));
|
|
3593
3640
|
}
|
|
3594
3641
|
/** iOS Beagle sends files inline as a JSON envelope over (bulk)messages:
|
|
3595
3642
|
* {"data":"<base64>","fileExtension":".jpg","fileName":"…","type":"image"}.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
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",
|