@decentnetwork/peer 0.1.156 → 0.1.158
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 +22 -1
- package/dist/peer.js +99 -11
- package/package.json +1 -1
package/dist/peer.d.ts
CHANGED
|
@@ -97,7 +97,28 @@ export declare class Peer {
|
|
|
97
97
|
routes: number;
|
|
98
98
|
targets: number;
|
|
99
99
|
} | undefined;
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Accept a friend request.
|
|
102
|
+
*
|
|
103
|
+
* `#pendingFriendRequests` is an in-memory cache, not the authority on what
|
|
104
|
+
* was asked. A host that remembers requests across a restart — beagle-web
|
|
105
|
+
* keeps them in IndexedDB, because a request usually arrives while the tab is
|
|
106
|
+
* closed — is doing the right thing, and refusing it here made every restored
|
|
107
|
+
* card permanently un-acceptable: click Accept, the SDK throws "no pending
|
|
108
|
+
* friend request", the card comes back, and the button reads as dead. That is
|
|
109
|
+
* the whole of the reported "Accept does not respond" on app.beagle.chat.
|
|
110
|
+
*
|
|
111
|
+
* Accepting is the host's decision. Take it, and let the host pass back what
|
|
112
|
+
* it remembered so the friend does not arrive nameless — anything missing is
|
|
113
|
+
* filled in by the userinfo exchange once the session comes up.
|
|
114
|
+
*/
|
|
115
|
+
acceptFriendRequest(pubkey: string, opts?: {
|
|
116
|
+
name?: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
hello?: string;
|
|
119
|
+
address?: string;
|
|
120
|
+
nospam?: number;
|
|
121
|
+
}): Promise<void>;
|
|
101
122
|
/** Decline an inbound request.
|
|
102
123
|
*
|
|
103
124
|
* Also drops any friend record and session the request already created.
|
package/dist/peer.js
CHANGED
|
@@ -527,6 +527,10 @@ export class Peer {
|
|
|
527
527
|
// #initiateSession every few seconds for every friend; without this
|
|
528
528
|
// dedupe, lower-pubkey side floods the log with "deferring to peer".
|
|
529
529
|
#initiateSkipLogged = new Set();
|
|
530
|
+
/** Recently COMPLETED bulkmsg assemblies, same key, kept for the same 60s
|
|
531
|
+
* the assembly itself lives. Without it a retransmitted message reassembles
|
|
532
|
+
* a second time and is delivered twice. */
|
|
533
|
+
#bulkCompleted = new Map();
|
|
530
534
|
// In-flight Carrier BULKMSG reassemblies, keyed `${friendId}:${tid}`.
|
|
531
535
|
// Fragments are appended in arrival order (the lossless stream keeps
|
|
532
536
|
// them ordered) and the message completes when totalsz bytes arrived.
|
|
@@ -1241,20 +1245,49 @@ export class Peer {
|
|
|
1241
1245
|
lastFriendRequestDispatch() {
|
|
1242
1246
|
return this.#lastFriendRequestDispatch;
|
|
1243
1247
|
}
|
|
1244
|
-
|
|
1248
|
+
/**
|
|
1249
|
+
* Accept a friend request.
|
|
1250
|
+
*
|
|
1251
|
+
* `#pendingFriendRequests` is an in-memory cache, not the authority on what
|
|
1252
|
+
* was asked. A host that remembers requests across a restart — beagle-web
|
|
1253
|
+
* keeps them in IndexedDB, because a request usually arrives while the tab is
|
|
1254
|
+
* closed — is doing the right thing, and refusing it here made every restored
|
|
1255
|
+
* card permanently un-acceptable: click Accept, the SDK throws "no pending
|
|
1256
|
+
* friend request", the card comes back, and the button reads as dead. That is
|
|
1257
|
+
* the whole of the reported "Accept does not respond" on app.beagle.chat.
|
|
1258
|
+
*
|
|
1259
|
+
* Accepting is the host's decision. Take it, and let the host pass back what
|
|
1260
|
+
* it remembered so the friend does not arrive nameless — anything missing is
|
|
1261
|
+
* filled in by the userinfo exchange once the session comes up.
|
|
1262
|
+
*/
|
|
1263
|
+
async acceptFriendRequest(pubkey, opts) {
|
|
1245
1264
|
const request = this.#pendingFriendRequests.get(pubkey);
|
|
1246
|
-
if (!request) {
|
|
1247
|
-
|
|
1265
|
+
if (!request && !opts) {
|
|
1266
|
+
// No cached request AND the caller told us nothing: it can still be
|
|
1267
|
+
// accepted, but say so, because it means the host is not passing on what
|
|
1268
|
+
// it knows and the friend will start out anonymous.
|
|
1269
|
+
this.#debugLog(`accepting ${pubkey} with no cached request and no details from the host`);
|
|
1270
|
+
}
|
|
1271
|
+
let key;
|
|
1272
|
+
try {
|
|
1273
|
+
key = base58ToBytes(pubkey);
|
|
1248
1274
|
}
|
|
1275
|
+
catch {
|
|
1276
|
+
throw new Error(`Not a valid peer key: ${pubkey}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (key.length !== 32) {
|
|
1279
|
+
throw new Error(`Not a valid peer key: ${pubkey}`);
|
|
1280
|
+
}
|
|
1281
|
+
const nospam = request?.nospam ?? opts?.nospam ?? 0;
|
|
1249
1282
|
this.#pendingFriendRequests.delete(pubkey);
|
|
1250
1283
|
this.#friends.set(pubkey, {
|
|
1251
1284
|
pubkey,
|
|
1252
|
-
userid: request
|
|
1253
|
-
address: request
|
|
1254
|
-
nospam
|
|
1255
|
-
name: request
|
|
1256
|
-
description: request
|
|
1257
|
-
hello: request
|
|
1285
|
+
userid: request?.userid ?? pubkey,
|
|
1286
|
+
address: request?.address ?? opts?.address ?? carrierAddressFromPublicKey(key, nospam),
|
|
1287
|
+
nospam,
|
|
1288
|
+
name: request?.name ?? opts?.name ?? "",
|
|
1289
|
+
description: request?.description ?? opts?.description ?? "",
|
|
1290
|
+
hello: request?.hello ?? opts?.hello ?? "",
|
|
1258
1291
|
status: "offline",
|
|
1259
1292
|
acceptedAt: Date.now()
|
|
1260
1293
|
});
|
|
@@ -1613,6 +1646,29 @@ export class Peer {
|
|
|
1613
1646
|
return;
|
|
1614
1647
|
}
|
|
1615
1648
|
}
|
|
1649
|
+
// Inline files ride the TEXT channel, and the delivery envelope wraps them.
|
|
1650
|
+
//
|
|
1651
|
+
// The call sites check for an inline file BEFORE this function, i.e. before
|
|
1652
|
+
// the envelope comes off — so they were looking at "\x1eDNPACK1:<base64>",
|
|
1653
|
+
// which does not start with "{", and every inline file sent with delivery
|
|
1654
|
+
// tracking was reported as a text message instead. Measured 2026-08-31,
|
|
1655
|
+
// peer to peer: a 95-byte PNG arrived as 200 characters of JSON, and a
|
|
1656
|
+
// 300 KB one as 409,672 characters. Both transferred perfectly; both were
|
|
1657
|
+
// handed to the application as a wall of base64.
|
|
1658
|
+
//
|
|
1659
|
+
// The check belongs here, on the unwrapped text, where it sees what the
|
|
1660
|
+
// sender actually wrote. The call-site checks stay for envelope-less
|
|
1661
|
+
// senders (iOS and Android do not speak this envelope).
|
|
1662
|
+
//
|
|
1663
|
+
// ACK first: an inline file is delivered, and skipping the ACK because we
|
|
1664
|
+
// did not run the text handlers would make the sender retransmit forever.
|
|
1665
|
+
if (this.#tryEmitInlineFile(msg.pubkey, text, msg.via === "offline" ? "offline" : "online")) {
|
|
1666
|
+
if (deliveryId) {
|
|
1667
|
+
this.#rememberDeliveredTextId(deliveryId);
|
|
1668
|
+
void this.#sendTextAck(msg.pubkey, deliveryId).catch(() => undefined);
|
|
1669
|
+
}
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1616
1672
|
if (this.#textHandlers.size === 0) {
|
|
1617
1673
|
if (deliveryId)
|
|
1618
1674
|
this.#debugLog(`text delivery ${deliveryId} from ${msg.pubkey} has no onText handlers; not ACKing`);
|
|
@@ -3179,13 +3235,22 @@ export class Peer {
|
|
|
3179
3235
|
try {
|
|
3180
3236
|
const decoded = decodeCarrierPacket(carrierPacket);
|
|
3181
3237
|
if (decoded.type !== PACKET_TYPE_FRIEND_REQUEST) {
|
|
3238
|
+
// Was a bare `return`. An onion friend-request that arrived and did not
|
|
3239
|
+
// parse left NOTHING anywhere — the sender saw success, the recipient
|
|
3240
|
+
// saw no request, and there was no way to tell "never arrived" from
|
|
3241
|
+
// "arrived and was dropped". That is the single most-reported symptom
|
|
3242
|
+
// on this project and it was undiagnosable by construction.
|
|
3243
|
+
this.#debugLog(`friend-request from ${carrierIdFromPublicKey(senderPublicKey)} DROPPED: ` +
|
|
3244
|
+
`decoded as packet type ${decoded.type}, expected ${PACKET_TYPE_FRIEND_REQUEST}`);
|
|
3182
3245
|
return;
|
|
3183
3246
|
}
|
|
3184
3247
|
hello = decoded.hello;
|
|
3185
3248
|
name = decoded.name;
|
|
3186
3249
|
description = decoded.descr;
|
|
3187
3250
|
}
|
|
3188
|
-
catch {
|
|
3251
|
+
catch (error) {
|
|
3252
|
+
this.#debugLog(`friend-request from ${carrierIdFromPublicKey(senderPublicKey)} DROPPED: ` +
|
|
3253
|
+
`undecodable (${carrierPacket.length} bytes): ${error.message}`);
|
|
3189
3254
|
return;
|
|
3190
3255
|
}
|
|
3191
3256
|
const userid = carrierIdFromPublicKey(senderPublicKey);
|
|
@@ -3227,13 +3292,17 @@ export class Peer {
|
|
|
3227
3292
|
try {
|
|
3228
3293
|
const decoded = decodeCarrierPacket(packet);
|
|
3229
3294
|
if (decoded.type !== PACKET_TYPE_FRIEND_REQUEST) {
|
|
3295
|
+
this.#debugLog(`offline friend-request from ${fromUserId} DROPPED: decoded as packet ` +
|
|
3296
|
+
`type ${decoded.type}, expected ${PACKET_TYPE_FRIEND_REQUEST}`);
|
|
3230
3297
|
return;
|
|
3231
3298
|
}
|
|
3232
3299
|
helloText = decoded.hello;
|
|
3233
3300
|
name = decoded.name;
|
|
3234
3301
|
description = decoded.descr;
|
|
3235
3302
|
}
|
|
3236
|
-
catch {
|
|
3303
|
+
catch (error) {
|
|
3304
|
+
this.#debugLog(`offline friend-request from ${fromUserId} DROPPED: undecodable ` +
|
|
3305
|
+
`(${packet.length} bytes): ${error.message}`);
|
|
3237
3306
|
return;
|
|
3238
3307
|
}
|
|
3239
3308
|
// Already an established friend? Ignore this (re-)request. Auto-friend
|
|
@@ -4763,6 +4832,24 @@ export class Peer {
|
|
|
4763
4832
|
this.#bulkAssembly.delete(key);
|
|
4764
4833
|
}
|
|
4765
4834
|
const key = `${friendId}:${frag.tid.toString()}`;
|
|
4835
|
+
// A completed assembly is deleted, so a second copy of the same fragments
|
|
4836
|
+
// starts a fresh entry and completes AGAIN — the application gets the same
|
|
4837
|
+
// message twice. Duplicate FRAGMENTS were already ignored; a duplicate
|
|
4838
|
+
// whole MESSAGE was not.
|
|
4839
|
+
//
|
|
4840
|
+
// It shows up on the plain send path (a native peer, or a JS peer before
|
|
4841
|
+
// userinfo has settled), because that path carries no delivery id and the
|
|
4842
|
+
// text-level dedupe has nothing to key on. Measured 2026-08-31: one
|
|
4843
|
+
// sendInlineFile call, two `bulkmsg complete (409672 bytes)` and two
|
|
4844
|
+
// identical 300 KB images delivered.
|
|
4845
|
+
for (const [k, at] of this.#bulkCompleted) {
|
|
4846
|
+
if (at < now - 60_000)
|
|
4847
|
+
this.#bulkCompleted.delete(k);
|
|
4848
|
+
}
|
|
4849
|
+
if (this.#bulkCompleted.has(key)) {
|
|
4850
|
+
this.#debugLog(`bulkmsg ${key} already completed — dropping duplicate`);
|
|
4851
|
+
return undefined;
|
|
4852
|
+
}
|
|
4766
4853
|
let entry = this.#bulkAssembly.get(key);
|
|
4767
4854
|
if (!entry) {
|
|
4768
4855
|
entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + 60_000 };
|
|
@@ -4787,6 +4874,7 @@ export class Peer {
|
|
|
4787
4874
|
if (entry.total === 0 || entry.got < entry.total)
|
|
4788
4875
|
return undefined;
|
|
4789
4876
|
this.#bulkAssembly.delete(key);
|
|
4877
|
+
this.#bulkCompleted.set(key, now);
|
|
4790
4878
|
if (entry.got > entry.total) {
|
|
4791
4879
|
this.#debugLog(`bulkmsg from ${friendId} got ${entry.got} > total ${entry.total} — dropped`);
|
|
4792
4880
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.158",
|
|
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",
|