@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 CHANGED
@@ -97,7 +97,28 @@ export declare class Peer {
97
97
  routes: number;
98
98
  targets: number;
99
99
  } | undefined;
100
- acceptFriendRequest(pubkey: string): Promise<void>;
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
- async acceptFriendRequest(pubkey) {
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
- throw new Error("No pending friend request from this peer");
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.userid,
1253
- address: request.address,
1254
- nospam: request.nospam,
1255
- name: request.name,
1256
- description: request.description,
1257
- hello: request.hello,
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.156",
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",