@decentnetwork/peer 0.1.85 → 0.1.87
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/compat/packet.d.ts +38 -0
- package/dist/compat/packet.js +66 -3
- package/dist/crypto/rendezvous.d.ts +7 -0
- package/dist/crypto/rendezvous.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/peer.d.ts +5 -0
- package/dist/peer.js +416 -232
- package/dist/store/friends.d.ts +8 -0
- package/dist/types/peer.d.ts +22 -0
- package/package.json +3 -1
package/dist/compat/packet.d.ts
CHANGED
|
@@ -20,6 +20,18 @@ export type UserInfoPacket = {
|
|
|
20
20
|
gender: string;
|
|
21
21
|
email: string;
|
|
22
22
|
region: string;
|
|
23
|
+
/** AgentNet wire-protocol version (userinfo field 7, appended). 0 = a legacy
|
|
24
|
+
* peer that predates the field (native C SDK before the extension, or an old
|
|
25
|
+
* JS peer). Non-zero lets peers negotiate capabilities (e.g. toxcore file
|
|
26
|
+
* transfer for large files vs. inline bulkmsg). */
|
|
27
|
+
protoVersion: number;
|
|
28
|
+
/** Client platform (field 8): "ios" | "android" | "js" | "linux" | "darwin"
|
|
29
|
+
* | "win32" | "" (unknown/legacy). */
|
|
30
|
+
platform: string;
|
|
31
|
+
/** OS/runtime version string (field 9), e.g. "17.5", "14", "node-24.2.0". */
|
|
32
|
+
osVersion: string;
|
|
33
|
+
/** Application/SDK version (field 10), e.g. "beagle-2.3.1", "lan-0.1.166". */
|
|
34
|
+
appVersion: string;
|
|
23
35
|
};
|
|
24
36
|
export type FriendRequestPacket = {
|
|
25
37
|
type: typeof PACKET_TYPE_FRIEND_REQUEST;
|
|
@@ -87,6 +99,13 @@ export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessage
|
|
|
87
99
|
* field 4 = gender : string
|
|
88
100
|
* field 5 = email : string
|
|
89
101
|
* field 6 = region : string
|
|
102
|
+
*
|
|
103
|
+
* AgentNet extends the table with appended fields (FlatBuffers is forward/
|
|
104
|
+
* backward compatible — a peer that predates a field just reads its default):
|
|
105
|
+
* field 7 = proto_version : uint (AgentNet wire-protocol version, 0=legacy)
|
|
106
|
+
* field 8 = platform : string ("ios"/"android"/"js"/"darwin"/"linux"/…)
|
|
107
|
+
* field 9 = os_version : string
|
|
108
|
+
* field 10 = app_version : string
|
|
90
109
|
*/
|
|
91
110
|
export declare function encodeUserInfoPacket(opts: {
|
|
92
111
|
avatar?: boolean;
|
|
@@ -96,6 +115,10 @@ export declare function encodeUserInfoPacket(opts: {
|
|
|
96
115
|
gender?: string;
|
|
97
116
|
email?: string;
|
|
98
117
|
region?: string;
|
|
118
|
+
protoVersion?: number;
|
|
119
|
+
platform?: string;
|
|
120
|
+
osVersion?: string;
|
|
121
|
+
appVersion?: string;
|
|
99
122
|
}): Uint8Array;
|
|
100
123
|
export declare function encodeFriendRequestPacket(opts: {
|
|
101
124
|
name?: string;
|
|
@@ -144,3 +167,18 @@ export declare function encodeInviteRspPacket(opts: {
|
|
|
144
167
|
data?: Uint8Array;
|
|
145
168
|
}): Uint8Array;
|
|
146
169
|
export declare function decodeCarrierPacket(bytes: Uint8Array): CarrierPacket;
|
|
170
|
+
/**
|
|
171
|
+
* Encode a toxcore net_crypto PACKET_ID_REQUEST payload requesting retransmit
|
|
172
|
+
* of the packets MISSING in the receive window `[low, high]` (inclusive). This
|
|
173
|
+
* is the exact inverse of the decoder in peer.ts `#handleRetransmitRequest`:
|
|
174
|
+
* walk each number from `low`, keep a running counter `n` (starts at 1), and
|
|
175
|
+
* emit `n` at every MISSING number (then reset it), inserting a 0 filler byte
|
|
176
|
+
* whenever the counter reaches 255. `have(num)` returns true if we already hold
|
|
177
|
+
* that packet number. `low` is always treated as missing by construction (it is
|
|
178
|
+
* the low-water mark — the first not-yet-delivered number). Numbers are 32-bit
|
|
179
|
+
* and wrap; the window is small so the walk is bounded.
|
|
180
|
+
*
|
|
181
|
+
* Returned bytes do NOT include the leading PACKET_ID_REQUEST kind byte — the
|
|
182
|
+
* caller prepends it.
|
|
183
|
+
*/
|
|
184
|
+
export declare function encodeRetransmitRequest(low: number, high: number, have: (num: number) => boolean): Uint8Array;
|
package/dist/compat/packet.js
CHANGED
|
@@ -10,7 +10,12 @@ export const PACKET_TYPE_BULKMSG = 36;
|
|
|
10
10
|
// of <=1024 raw bytes sharing one tid (totalsz set on the FIRST fragment
|
|
11
11
|
// only), reassembled in arrival order by the receiver.
|
|
12
12
|
export const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
|
|
13
|
-
|
|
13
|
+
// Raised 5 MB -> 16 MB (matches the native SDK) so inline file sends carry
|
|
14
|
+
// ~11 MB real files. The receive reorder buffer + retransmit (peer.ts) keep the
|
|
15
|
+
// many fragments in order; the reliable window still bounds lossy-path
|
|
16
|
+
// throughput — very large / streaming transfers should use sendFile() (toxcore
|
|
17
|
+
// messenger file transfer), not inline bulkmsg.
|
|
18
|
+
export const CARRIER_MAX_APP_BULKMSG_LEN = 16 * 1024 * 1024;
|
|
14
19
|
// Carrier friend-invite constants (carrier.c / carrier.h). An invite carries
|
|
15
20
|
// up to 8192 bytes of application data, fragmented into INVITE_DATA_UNIT-byte
|
|
16
21
|
// chunks sharing one tid — the first fragment carries totalsz (and the
|
|
@@ -48,6 +53,13 @@ const ANYBODY_BULKMSG = 6;
|
|
|
48
53
|
* field 4 = gender : string
|
|
49
54
|
* field 5 = email : string
|
|
50
55
|
* field 6 = region : string
|
|
56
|
+
*
|
|
57
|
+
* AgentNet extends the table with appended fields (FlatBuffers is forward/
|
|
58
|
+
* backward compatible — a peer that predates a field just reads its default):
|
|
59
|
+
* field 7 = proto_version : uint (AgentNet wire-protocol version, 0=legacy)
|
|
60
|
+
* field 8 = platform : string ("ios"/"android"/"js"/"darwin"/"linux"/…)
|
|
61
|
+
* field 9 = os_version : string
|
|
62
|
+
* field 10 = app_version : string
|
|
51
63
|
*/
|
|
52
64
|
export function encodeUserInfoPacket(opts) {
|
|
53
65
|
const builder = new Builder(256);
|
|
@@ -57,8 +69,21 @@ export function encodeUserInfoPacket(opts) {
|
|
|
57
69
|
const gender = builder.createString(opts.gender ?? "");
|
|
58
70
|
const email = builder.createString(opts.email ?? "");
|
|
59
71
|
const region = builder.createString(opts.region ?? "");
|
|
60
|
-
|
|
72
|
+
// Only emit the AgentNet extension strings when set (an all-legacy userinfo
|
|
73
|
+
// stays byte-identical to what the C SDK sends).
|
|
74
|
+
const platform = opts.platform ? builder.createString(opts.platform) : 0;
|
|
75
|
+
const osVersion = opts.osVersion ? builder.createString(opts.osVersion) : 0;
|
|
76
|
+
const appVersion = opts.appVersion ? builder.createString(opts.appVersion) : 0;
|
|
77
|
+
builder.startObject(11);
|
|
61
78
|
// Add high field numbers first so vtable reflects all slots.
|
|
79
|
+
if (appVersion)
|
|
80
|
+
builder.addFieldOffset(10, appVersion, 0);
|
|
81
|
+
if (osVersion)
|
|
82
|
+
builder.addFieldOffset(9, osVersion, 0);
|
|
83
|
+
if (platform)
|
|
84
|
+
builder.addFieldOffset(8, platform, 0);
|
|
85
|
+
if (opts.protoVersion)
|
|
86
|
+
builder.addFieldInt32(7, opts.protoVersion >>> 0, 0);
|
|
62
87
|
builder.addFieldOffset(6, region, 0);
|
|
63
88
|
builder.addFieldOffset(5, email, 0);
|
|
64
89
|
builder.addFieldOffset(4, gender, 0);
|
|
@@ -205,7 +230,12 @@ export function decodeCarrierPacket(bytes) {
|
|
|
205
230
|
phone: readStringField(body, 3) ?? "",
|
|
206
231
|
gender: readStringField(body, 4) ?? "",
|
|
207
232
|
email: readStringField(body, 5) ?? "",
|
|
208
|
-
region: readStringField(body, 6) ?? ""
|
|
233
|
+
region: readStringField(body, 6) ?? "",
|
|
234
|
+
// Appended AgentNet fields — absent (default) from legacy peers.
|
|
235
|
+
protoVersion: readUint32Field(body, 7),
|
|
236
|
+
platform: readStringField(body, 8) ?? "",
|
|
237
|
+
osVersion: readStringField(body, 9) ?? "",
|
|
238
|
+
appVersion: readStringField(body, 10) ?? ""
|
|
209
239
|
};
|
|
210
240
|
}
|
|
211
241
|
if (type === PACKET_TYPE_FRIEND_REQUEST && bodyType === ANYBODY_FRIENDREQ) {
|
|
@@ -312,3 +342,36 @@ function readByteVectorField(table, field) {
|
|
|
312
342
|
const length = table.bb.__vector_len(table.bb_pos + offset);
|
|
313
343
|
return table.bb.bytes().slice(vector, vector + length);
|
|
314
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Encode a toxcore net_crypto PACKET_ID_REQUEST payload requesting retransmit
|
|
347
|
+
* of the packets MISSING in the receive window `[low, high]` (inclusive). This
|
|
348
|
+
* is the exact inverse of the decoder in peer.ts `#handleRetransmitRequest`:
|
|
349
|
+
* walk each number from `low`, keep a running counter `n` (starts at 1), and
|
|
350
|
+
* emit `n` at every MISSING number (then reset it), inserting a 0 filler byte
|
|
351
|
+
* whenever the counter reaches 255. `have(num)` returns true if we already hold
|
|
352
|
+
* that packet number. `low` is always treated as missing by construction (it is
|
|
353
|
+
* the low-water mark — the first not-yet-delivered number). Numbers are 32-bit
|
|
354
|
+
* and wrap; the window is small so the walk is bounded.
|
|
355
|
+
*
|
|
356
|
+
* Returned bytes do NOT include the leading PACKET_ID_REQUEST kind byte — the
|
|
357
|
+
* caller prepends it.
|
|
358
|
+
*/
|
|
359
|
+
export function encodeRetransmitRequest(low, high, have) {
|
|
360
|
+
const out = [];
|
|
361
|
+
let n = 1;
|
|
362
|
+
const end = (high + 1) >>> 0;
|
|
363
|
+
for (let i = low >>> 0; i !== end; i = (i + 1) >>> 0) {
|
|
364
|
+
if (!have(i)) {
|
|
365
|
+
out.push(n & 0xff);
|
|
366
|
+
n = 0;
|
|
367
|
+
}
|
|
368
|
+
if (n === 255) {
|
|
369
|
+
out.push(0);
|
|
370
|
+
n = 1;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
n++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return Uint8Array.from(out);
|
|
377
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const RENDEZVOUS_AUTH_KEY_DOMAIN = "RSP-CARRIER-AUTH-KEY/1";
|
|
2
|
+
export declare const RENDEZVOUS_AUTH_PROOF_LENGTH = 32;
|
|
3
|
+
/**
|
|
4
|
+
* Create an RSP/1 Carrier identity proof without starting Carrier discovery.
|
|
5
|
+
* The secret key remains caller-owned and temporary shared material is wiped.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createCarrierRendezvousAuthProof(identitySecretKey: Uint8Array, serverEphemeralPublicKey: Uint8Array, authTranscript: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import nacl from "tweetnacl";
|
|
3
|
+
export const RENDEZVOUS_AUTH_KEY_DOMAIN = "RSP-CARRIER-AUTH-KEY/1";
|
|
4
|
+
export const RENDEZVOUS_AUTH_PROOF_LENGTH = 32;
|
|
5
|
+
/**
|
|
6
|
+
* Create an RSP/1 Carrier identity proof without starting Carrier discovery.
|
|
7
|
+
* The secret key remains caller-owned and temporary shared material is wiped.
|
|
8
|
+
*/
|
|
9
|
+
export function createCarrierRendezvousAuthProof(identitySecretKey, serverEphemeralPublicKey, authTranscript) {
|
|
10
|
+
if (identitySecretKey.length !== nacl.box.secretKeyLength) {
|
|
11
|
+
throw new RangeError(`identitySecretKey must be ${nacl.box.secretKeyLength} bytes`);
|
|
12
|
+
}
|
|
13
|
+
if (serverEphemeralPublicKey.length !== nacl.box.publicKeyLength) {
|
|
14
|
+
throw new RangeError(`serverEphemeralPublicKey must be ${nacl.box.publicKeyLength} bytes`);
|
|
15
|
+
}
|
|
16
|
+
if (authTranscript.length === 0)
|
|
17
|
+
throw new RangeError("authTranscript must not be empty");
|
|
18
|
+
const rawShared = nacl.scalarMult(identitySecretKey, serverEphemeralPublicKey);
|
|
19
|
+
const isLowOrder = rawShared.every((byte) => byte === 0);
|
|
20
|
+
rawShared.fill(0);
|
|
21
|
+
if (isLowOrder)
|
|
22
|
+
throw new Error("serverEphemeralPublicKey is low order");
|
|
23
|
+
const shared = nacl.box.before(serverEphemeralPublicKey, identitySecretKey);
|
|
24
|
+
const authKey = createHmac("sha256", Buffer.from(shared)).update(RENDEZVOUS_AUTH_KEY_DOMAIN, "utf8").digest();
|
|
25
|
+
shared.fill(0);
|
|
26
|
+
try {
|
|
27
|
+
return Uint8Array.from(createHmac("sha256", authKey).update(authTranscript).digest());
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
authKey.fill(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, PACKET_TYPE_INVITE_REQ
|
|
|
4
4
|
export { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, CRYPTO_PACKET_NAT_PING, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
5
5
|
export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_REQUEST, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID } from "./compat/tox-onion.js";
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
|
7
|
+
export { createCarrierRendezvousAuthProof, RENDEZVOUS_AUTH_KEY_DOMAIN, RENDEZVOUS_AUTH_PROOF_LENGTH } from "./crypto/rendezvous.js";
|
|
7
8
|
export { base58ToBytes, bytesToBase58 } from "./utils/base58.js";
|
|
8
9
|
export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
|
|
9
10
|
export type { CarrierPacket, FriendMessagePacket, FriendRequestPacket, InviteReqPacket, InviteRspPacket } from "./compat/packet.js";
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, PACKET_TYPE_INVITE_REQ
|
|
|
4
4
|
export { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, CRYPTO_PACKET_NAT_PING, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
5
5
|
export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_REQUEST, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID } from "./compat/tox-onion.js";
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
|
7
|
+
export { createCarrierRendezvousAuthProof, RENDEZVOUS_AUTH_KEY_DOMAIN, RENDEZVOUS_AUTH_PROOF_LENGTH } from "./crypto/rendezvous.js";
|
|
7
8
|
export { base58ToBytes, bytesToBase58 } from "./utils/base58.js";
|
|
8
9
|
export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
|
package/dist/peer.d.ts
CHANGED
|
@@ -45,6 +45,11 @@ export declare class Peer {
|
|
|
45
45
|
selfAnnounceStoredOn: number;
|
|
46
46
|
udpLocalPort: number | null;
|
|
47
47
|
tcpRelayConnected: number;
|
|
48
|
+
/** TCP-relay onion diagnostics: requests handed to relays / responses
|
|
49
|
+
* routed back. If sent>0 but recv=0, relays aren't returning onion
|
|
50
|
+
* responses (relay doesn't forward, or wire mismatch). */
|
|
51
|
+
tcpOnionSent: number;
|
|
52
|
+
tcpOnionRecv: number;
|
|
48
53
|
/** The specific relay endpoints this peer has live connections to.
|
|
49
54
|
* Lets us compare two peers' relay sets — if there's NO overlap,
|
|
50
55
|
* net_crypto handshake packets between them have nowhere to route
|
package/dist/peer.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import nacl from "tweetnacl";
|
|
6
6
|
import { carrierAddressFromPublicKey, carrierIdFromAddress, carrierIdFromPublicKey, parseCarrierAddress } from "./compat/address.js";
|
|
7
7
|
import { LegacyBootstrapClient } from "./compat/bootstrap.js";
|
|
8
|
-
import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, PACKET_TYPE_BULKMSG, PACKET_TYPE_INVITE_REQUEST, PACKET_TYPE_INVITE_RESPONSE, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, INVITE_DATA_UNIT, CARRIER_MAX_INVITE_DATA_LEN, CARRIER_EXTENSION_NAME, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket, encodeInviteReqPacket } from "./compat/packet.js";
|
|
8
|
+
import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, PACKET_TYPE_BULKMSG, PACKET_TYPE_INVITE_REQUEST, PACKET_TYPE_INVITE_RESPONSE, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, INVITE_DATA_UNIT, CARRIER_MAX_INVITE_DATA_LEN, CARRIER_EXTENSION_NAME, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket, encodeInviteReqPacket, encodeRetransmitRequest } from "./compat/packet.js";
|
|
9
9
|
import { NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID, createOnionAnnounceRequest, createOnionDataPacket, createOnionDataRequest, createOnionRequest0, createOnionRequest0Tcp, openOnionAnnounceResponse, openOnionDataPacket, openOnionDataResponse } from "./compat/tox-onion.js";
|
|
10
10
|
import { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
11
11
|
import { NET_PACKET_PING_REQUEST, NET_PACKET_PING_RESPONSE, NET_PACKET_GET_NODES, NET_PACKET_SEND_NODES, encodeDhtRpc, decodeDhtRpc, buildPingPlain, parsePingPlain, buildGetNodesPlain, parseGetNodesPlain, buildSendNodesPlain, parseSendNodesNodeBytes, packUdpNodeV4 } from "./compat/dht-rpc.js";
|
|
@@ -165,6 +165,16 @@ const LAN_DISCOVERY_PORTS = (process.env.DECENT_LAN_DISCOVERY_PORTS ?? "33445,33
|
|
|
165
165
|
const PEER_NICKNAME = process.env.DECENT_PEER_NAME ?? "@decentnetwork/peer";
|
|
166
166
|
const PEER_STATUS_MESSAGE = process.env.DECENT_PEER_STATUS_MESSAGE ?? "decent peer";
|
|
167
167
|
const GREETING_TEXT = process.env.DECENT_GREETING_TEXT ?? "";
|
|
168
|
+
// AgentNet wire-protocol version advertised in the userinfo profile (field 7).
|
|
169
|
+
// Bump when the wire capabilities change so peers can negotiate. v1 marks a peer
|
|
170
|
+
// that carries this field AND supports the toxcore file-transfer channel
|
|
171
|
+
// (PACKET_ID_FILE 80-82) for large files — a sender can prefer that over inline
|
|
172
|
+
// bulkmsg (capped at CARRIER_MAX_APP_BULKMSG_LEN) when the peer's protoVersion>=1.
|
|
173
|
+
const AGENTNET_PROTO_VERSION = 1;
|
|
174
|
+
// Peer package version, advertised as the default appVersion when the embedder
|
|
175
|
+
// doesn't override it. Read lazily so a bundler that inlines this file doesn't
|
|
176
|
+
// need the package.json at runtime.
|
|
177
|
+
const PEER_PKG_VERSION = "0.1.87";
|
|
168
178
|
// Toxcore Messenger.h packet IDs (live inside encrypted 0x1b crypto data plain payload)
|
|
169
179
|
const PACKET_ID_PADDING = 0;
|
|
170
180
|
const PACKET_ID_REQUEST = 1; // request retransmission of unreceived packets
|
|
@@ -186,6 +196,17 @@ const PACKET_ID_ACTION = 65; // /me action message
|
|
|
186
196
|
// understand this ID simply fall through and ignore it. Payload is 6
|
|
187
197
|
// bytes: 4-byte IPv4 + 2-byte big-endian port.
|
|
188
198
|
const PACKET_ID_UDP_ENDPOINT = 160;
|
|
199
|
+
// Receive reorder buffer bound. A reliable lossless packet more than this many
|
|
200
|
+
// numbers AHEAD of the contiguous low-water mark forces the stream forward
|
|
201
|
+
// (skipping the gap) rather than buffering unboundedly — a safety valve against
|
|
202
|
+
// a genuinely-lost packet that never retransmits wedging the whole stream (and
|
|
203
|
+
// against memory blowup). Sized generously (8192 * ~1KB ≈ 8 MB headroom) so a
|
|
204
|
+
// large multi-fragment inline file (16 MB bulkmsg ≈ 16k fragments) has ample
|
|
205
|
+
// grace for a lost fragment to be retransmitted before we give up on it.
|
|
206
|
+
const RECV_REORDER_WINDOW = 8192;
|
|
207
|
+
// Minimum gap between PACKET_ID_REQUEST packets we send for our own receive gaps
|
|
208
|
+
// (toxcore paces requests ~ once per RTT; this floor avoids flooding on a burst).
|
|
209
|
+
const RECV_REQUEST_MIN_INTERVAL_MS = 200;
|
|
189
210
|
export class Peer {
|
|
190
211
|
#opts;
|
|
191
212
|
#events = new EventEmitter();
|
|
@@ -280,6 +301,12 @@ export class Peer {
|
|
|
280
301
|
// 25s forever for stale persisted entries.
|
|
281
302
|
#dhtPkConsecutiveFailures = new Map();
|
|
282
303
|
#lastSelfAnnounceStoredCount = -1;
|
|
304
|
+
// Diagnostics for the TCP-relay onion path (announce/discovery over TCP).
|
|
305
|
+
// Surfaced in dhtHealth so `agentnet diag` shows whether it's active without
|
|
306
|
+
// needing verbose logs: sent = onion requests handed to relays, recv = onion
|
|
307
|
+
// responses routed back over TCP.
|
|
308
|
+
#diagTcpOnionSent = 0;
|
|
309
|
+
#diagTcpOnionRecv = 0;
|
|
283
310
|
// Per-friend last-logged route count from #discoverFriendRoutes so we
|
|
284
311
|
// only emit a "routes=N for friend=…" debug line when it changes.
|
|
285
312
|
#lastLoggedRoutesForFriend = new Map();
|
|
@@ -521,6 +548,8 @@ export class Peer {
|
|
|
521
548
|
this.#tcpRelays.on("onionResponse", (packet) => {
|
|
522
549
|
if (packet.length === 0)
|
|
523
550
|
return;
|
|
551
|
+
this.#diagTcpOnionRecv += 1;
|
|
552
|
+
this.#debugVerboseLog(`tcp onion response received (${packet.length} bytes, type=${packet[0]})`);
|
|
524
553
|
this.#udp.emit("datagram", {
|
|
525
554
|
data: Buffer.from(packet),
|
|
526
555
|
remote: { address: "tcp-relay", port: 0 },
|
|
@@ -686,6 +715,8 @@ export class Peer {
|
|
|
686
715
|
selfAnnounceStoredOn: this.#lastSelfAnnounceStoredCount,
|
|
687
716
|
udpLocalPort: this.#udp?.localPort() ?? null,
|
|
688
717
|
tcpRelayConnected: relays.length,
|
|
718
|
+
tcpOnionSent: this.#diagTcpOnionSent,
|
|
719
|
+
tcpOnionRecv: this.#diagTcpOnionRecv,
|
|
689
720
|
tcpRelayEndpoints: relays.map((r) => ({ host: r.host, port: r.port })),
|
|
690
721
|
};
|
|
691
722
|
}
|
|
@@ -1806,6 +1837,10 @@ export class Peer {
|
|
|
1806
1837
|
// colliding numbers). Drop them.
|
|
1807
1838
|
state.sendArray = undefined;
|
|
1808
1839
|
state.sendBufferStartNum = undefined;
|
|
1840
|
+
// Receive reorder buffer is keyed by the OLD stream's packet numbers —
|
|
1841
|
+
// discard it so stale fragments can't drain into the fresh stream.
|
|
1842
|
+
state.recvBuffer = undefined;
|
|
1843
|
+
state.lastRecvRequestMs = undefined;
|
|
1809
1844
|
}
|
|
1810
1845
|
else {
|
|
1811
1846
|
state.sendPacketNumber ??= 0;
|
|
@@ -2010,244 +2045,62 @@ export class Peer {
|
|
|
2010
2045
|
incrementNonce(state.peerBaseNonce);
|
|
2011
2046
|
}
|
|
2012
2047
|
}
|
|
2013
|
-
const kind = opened.payload[0];
|
|
2048
|
+
const kind = opened.payload[0] ?? -1;
|
|
2014
2049
|
const inner = opened.payload.slice(1);
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
//
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2050
|
+
// Route the decrypted packet by delivery class:
|
|
2051
|
+
// - REQUEST(1), lossy (>=192) and the app's droppable bulk channel
|
|
2052
|
+
// (bulkDataPacketId) do NOT consume a reliable send number on the
|
|
2053
|
+
// peer (toxcore sends them at buffer_end without storing them), so
|
|
2054
|
+
// they must NOT advance our ack point — deliver immediately.
|
|
2055
|
+
// - Single-transport reliable kinds — toxcore file transfer (80-82) —
|
|
2056
|
+
// carry their own reliability and are never dual-sent, so they also
|
|
2057
|
+
// deliver immediately (a reorder buffer would only add latency and
|
|
2058
|
+
// would wait forever for retransmits that arrive at fresh numbers);
|
|
2059
|
+
// they still advance the ack point eagerly, exactly as before.
|
|
2060
|
+
// - Everything else is the DUAL-SENT reliable message stream (chat 64,
|
|
2061
|
+
// bulkmsg, invites, profile, control): delivered STRICTLY in order
|
|
2062
|
+
// through a toxcore-style recv_array (below) so a reordered or
|
|
2063
|
+
// retransmitted fragment is neither delivered early nor dropped as a
|
|
2064
|
+
// duplicate. Advancing receiveBufferStart past a gap (the old bug)
|
|
2065
|
+
// ACKed a packet we never delivered, so the sender never retransmit —
|
|
2066
|
+
// corrupting every multi-fragment message (large inline files from
|
|
2067
|
+
// native peers surfaced as "bulkmsg totalsz 0 dropped").
|
|
2031
2068
|
const consumesReliableNumber = kind !== PACKET_ID_REQUEST &&
|
|
2032
|
-
|
|
2069
|
+
kind < 192 &&
|
|
2033
2070
|
kind !== this.#opts.bulkDataPacketId;
|
|
2034
|
-
if (consumesReliableNumber) {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
// Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
|
|
2038
|
-
if (this.#fileTransfer.handlePacket(friendId, kind, inner))
|
|
2039
|
-
return;
|
|
2040
|
-
if (kind === PACKET_ID_ONLINE) {
|
|
2041
|
-
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
2042
|
-
this.#debugLog(`crypto online packet received from ${friendId}`);
|
|
2043
|
-
// First time the friend signals ONLINE, push our profile
|
|
2044
|
-
// (nickname + status message) and any configured greeting so the
|
|
2045
|
-
// other side updates "unknown" to a real name. Toxcore semantics:
|
|
2046
|
-
// these are sent on every reconnect by the friend_connection layer.
|
|
2047
|
-
this.#sendProfileAndGreeting(friendId);
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
if (kind === PACKET_ID_OFFLINE) {
|
|
2051
|
-
this.#setFriendOffline(friendId);
|
|
2052
|
-
this.#debugLog(`crypto offline packet received from ${friendId}`);
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
if (kind === PACKET_ID_KILL) {
|
|
2056
|
-
this.#friendSessions.delete(friendId);
|
|
2057
|
-
this.#setFriendOffline(friendId);
|
|
2058
|
-
this.#debugLog(`crypto kill received from ${friendId}, session torn down`);
|
|
2059
|
-
return;
|
|
2060
|
-
}
|
|
2061
|
-
if (kind === PACKET_ID_ALIVE) {
|
|
2062
|
-
// Pure liveness signal; lastPingRecvMs already updated above.
|
|
2063
|
-
this.#debugVerboseLog(`crypto alive (keepalive) received from ${friendId}`);
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
if (kind === PACKET_ID_UDP_ENDPOINT) {
|
|
2067
|
-
// Peer told us their public UDP endpoint — hole-punch to it.
|
|
2068
|
-
this.#handleUdpEndpointOffer(friendId, inner);
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
if (kind === PACKET_ID_REQUEST) {
|
|
2072
|
-
// Lossless retransmission request (toxcore handle_request_packet).
|
|
2073
|
-
// Bytes are 1-based deltas walking our send window from
|
|
2074
|
-
// sendBufferStartNum: a matching delta marks that packet number
|
|
2075
|
-
// REQUESTED (peer is missing it — resend); every walked number
|
|
2076
|
-
// that doesn't match is implicitly ACKED (peer has it — drop from
|
|
2077
|
-
// the buffer). 0 is a +255 filler.
|
|
2078
|
-
this.#handleRetransmitRequest(friendId, state, inner);
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
if (kind === PACKET_ID_SHARE_RELAYS) {
|
|
2082
|
-
// Peer is sharing TCP relay endpoints they're connected to. We
|
|
2083
|
-
// don't have a TCP relay client yet, so cache nothing — just log.
|
|
2084
|
-
this.#debugVerboseLog(`crypto share-relays from ${friendId} (TCP relay client not implemented)`);
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
if (kind === PACKET_ID_NICKNAME) {
|
|
2088
|
-
const name = decodeUtf8Best(inner);
|
|
2089
|
-
const friend = this.#friends.get(friendId);
|
|
2090
|
-
if (friend && name && friend.name !== name) {
|
|
2091
|
-
this.#friends.set(friendId, { ...friend, name });
|
|
2092
|
-
this.#persistFriends();
|
|
2093
|
-
this.#events.emit("friendInfo", {
|
|
2094
|
-
pubkey: friendId,
|
|
2095
|
-
userid: friend.userid ?? friendId,
|
|
2096
|
-
name,
|
|
2097
|
-
description: friend.description
|
|
2098
|
-
});
|
|
2071
|
+
if (!consumesReliableNumber || isSingleTransportKind) {
|
|
2072
|
+
if (consumesReliableNumber) {
|
|
2073
|
+
state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
|
|
2099
2074
|
}
|
|
2100
|
-
this.#
|
|
2075
|
+
this.#deliverLosslessPayload(friendId, state, kind, inner, remote);
|
|
2101
2076
|
return;
|
|
2102
2077
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
//
|
|
2107
|
-
// may still send raw UTF-8, so try the FB decode first and fall
|
|
2108
|
-
// back to plain UTF-8 for compatibility.
|
|
2109
|
-
let userInfoName;
|
|
2110
|
-
let userInfoDescr;
|
|
2111
|
-
try {
|
|
2112
|
-
const decoded = decodeCarrierPacket(inner);
|
|
2113
|
-
if (decoded.type === PACKET_TYPE_USERINFO) {
|
|
2114
|
-
userInfoName = decoded.name;
|
|
2115
|
-
userInfoDescr = decoded.descr;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
catch {
|
|
2119
|
-
// Not a Carrier userinfo packet — fall through.
|
|
2120
|
-
}
|
|
2121
|
-
if (userInfoName !== undefined || userInfoDescr !== undefined) {
|
|
2122
|
-
const friend = this.#friends.get(friendId);
|
|
2123
|
-
const newName = userInfoName && userInfoName.length > 0 ? userInfoName : friend?.name;
|
|
2124
|
-
const newDescr = userInfoDescr ?? friend?.description;
|
|
2125
|
-
if (friend && (friend.name !== newName || friend.description !== newDescr)) {
|
|
2126
|
-
this.#friends.set(friendId, { ...friend, name: newName, description: newDescr });
|
|
2127
|
-
this.#persistFriends();
|
|
2128
|
-
this.#events.emit("friendInfo", {
|
|
2129
|
-
pubkey: friendId,
|
|
2130
|
-
userid: friend.userid ?? friendId,
|
|
2131
|
-
name: newName,
|
|
2132
|
-
description: newDescr
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
this.#debugLog(`crypto userinfo received from ${friendId}: name="${userInfoName ?? ""}" descr="${userInfoDescr ?? ""}"`);
|
|
2136
|
-
}
|
|
2137
|
-
else {
|
|
2138
|
-
const status = decodeUtf8Best(inner);
|
|
2139
|
-
const friend = this.#friends.get(friendId);
|
|
2140
|
-
if (friend && status && friend.description !== status) {
|
|
2141
|
-
this.#friends.set(friendId, { ...friend, description: status });
|
|
2142
|
-
this.#persistFriends();
|
|
2143
|
-
}
|
|
2144
|
-
this.#debugVerboseLog(`crypto status message (raw) received from ${friendId}: "${status}"`);
|
|
2145
|
-
}
|
|
2078
|
+
const expected = state.receiveBufferStart ?? 0;
|
|
2079
|
+
const ahead = (opened.packetNumber - expected) >>> 0;
|
|
2080
|
+
if (ahead >= 0x80000000) {
|
|
2081
|
+
// Below the low-water mark — already delivered. Duplicate; drop.
|
|
2146
2082
|
return;
|
|
2147
2083
|
}
|
|
2148
|
-
if (
|
|
2149
|
-
|
|
2084
|
+
if (ahead === 0) {
|
|
2085
|
+
// In order: deliver, advance, then drain any now-contiguous buffered.
|
|
2086
|
+
state.receiveBufferStart = (expected + 1) >>> 0;
|
|
2087
|
+
this.#deliverLosslessPayload(friendId, state, kind, inner, remote);
|
|
2088
|
+
this.#drainRecvBufferContiguous(friendId, state, remote);
|
|
2150
2089
|
return;
|
|
2151
2090
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2091
|
+
// A gap sits below this packet. Hold it and drive recovery: ask the
|
|
2092
|
+
// sender to retransmit the missing number(s). If the buffer outgrows the
|
|
2093
|
+
// reorder window the gap is a genuine loss that won't come back — force
|
|
2094
|
+
// the stream forward rather than wedge every later packet forever.
|
|
2095
|
+
const buf = (state.recvBuffer ??= new Map());
|
|
2096
|
+
if (!buf.has(opened.packetNumber))
|
|
2097
|
+
buf.set(opened.packetNumber, { kind, inner });
|
|
2098
|
+
if (buf.size > RECV_REORDER_WINDOW) {
|
|
2099
|
+
this.#forceAdvanceRecvBuffer(friendId, state, remote);
|
|
2155
2100
|
}
|
|
2156
|
-
|
|
2157
|
-
this.#
|
|
2158
|
-
// Decode the Carrier FlatBuffers wrapper when present. A BULKMSG
|
|
2159
|
-
// fragment (Carrier C splits any message >1024 bytes this way —
|
|
2160
|
-
// e.g. iOS Beagle images, which are FileModel JSON envelopes)
|
|
2161
|
-
// goes to the reassembler and only emits once complete; before
|
|
2162
|
-
// this, every fragment surfaced as a separate garbage "text".
|
|
2163
|
-
let text;
|
|
2164
|
-
let carrier;
|
|
2165
|
-
try {
|
|
2166
|
-
carrier = decodeCarrierPacket(inner);
|
|
2167
|
-
}
|
|
2168
|
-
catch { /* raw utf-8 peer */ }
|
|
2169
|
-
// Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST/RESPONSE = 34/35)
|
|
2170
|
-
// rides this same PACKET_ID_MESSAGE channel — it's what the iOS/Android
|
|
2171
|
-
// WebRTC SDK uses for call signaling via the "carrier" extension. Large
|
|
2172
|
-
// signals (SDP offers) fragment by tid like bulkmsg; reassemble then
|
|
2173
|
-
// surface as an "invite" event (NOT chat text).
|
|
2174
|
-
if (carrier?.type === PACKET_TYPE_INVITE_REQUEST) {
|
|
2175
|
-
const complete = this.#assembleInvite(friendId, carrier);
|
|
2176
|
-
if (!complete)
|
|
2177
|
-
return; // more fragments pending
|
|
2178
|
-
this.#events.emit("invite", {
|
|
2179
|
-
pubkey: friendId,
|
|
2180
|
-
ext: carrier.ext,
|
|
2181
|
-
bundle: carrier.bundle,
|
|
2182
|
-
data: complete
|
|
2183
|
-
});
|
|
2184
|
-
this.#debugLog(`invite from ${friendId}: ext="${carrier.ext ?? ""}" (${complete.length} bytes)`);
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
if (carrier?.type === PACKET_TYPE_INVITE_RESPONSE) {
|
|
2188
|
-
const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier) : new Uint8Array();
|
|
2189
|
-
if (carrier.status === 0 && !complete)
|
|
2190
|
-
return; // more fragments pending
|
|
2191
|
-
this.#events.emit("inviteResponse", {
|
|
2192
|
-
pubkey: friendId,
|
|
2193
|
-
ext: carrier.ext,
|
|
2194
|
-
bundle: carrier.bundle,
|
|
2195
|
-
status: carrier.status,
|
|
2196
|
-
reason: carrier.reason,
|
|
2197
|
-
data: complete ?? new Uint8Array()
|
|
2198
|
-
});
|
|
2199
|
-
this.#debugLog(`invite-response from ${friendId}: status=${carrier.status} ext="${carrier.ext ?? ""}"`);
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
if (carrier?.type === PACKET_TYPE_BULKMSG) {
|
|
2203
|
-
const complete = this.#assembleBulkMsg(friendId, carrier);
|
|
2204
|
-
if (!complete)
|
|
2205
|
-
return; // more fragments pending
|
|
2206
|
-
text = decodeUtf8Best(complete);
|
|
2207
|
-
this.#debugLog(`bulkmsg complete from ${friendId} (${complete.length} bytes)`);
|
|
2208
|
-
}
|
|
2209
|
-
else if (carrier?.type === PACKET_TYPE_MESSAGE) {
|
|
2210
|
-
text = new TextDecoder().decode(carrier.data);
|
|
2211
|
-
}
|
|
2212
|
-
else {
|
|
2213
|
-
text = decodeUtf8Best(inner);
|
|
2214
|
-
}
|
|
2215
|
-
// Carrier C peers terminate strings with a NUL — strip it so chat
|
|
2216
|
-
// text doesn't carry an invisible trailing byte.
|
|
2217
|
-
text = text?.replace(/\0+$/u, "");
|
|
2218
|
-
if (!text) {
|
|
2219
|
-
this.#debugLog(`crypto message packet decode failed from ${friendId}`);
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
// iOS Beagle sends files inline as a FileModel JSON envelope
|
|
2223
|
-
// ({data: base64, fileExtension, fileName, type}) — surface those
|
|
2224
|
-
// as files, not as a wall of base64 text.
|
|
2225
|
-
if (this.#tryEmitInlineFile(friendId, text, "online"))
|
|
2226
|
-
return;
|
|
2227
|
-
this.#events.emit("text", {
|
|
2228
|
-
pubkey: friendId,
|
|
2229
|
-
text,
|
|
2230
|
-
via: "online"
|
|
2231
|
-
});
|
|
2232
|
-
this.#debugLog(`crypto ${kind === PACKET_ID_ACTION ? "action" : "message"} from ${friendId}: "${text}"`);
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
if (kind >= 160 && kind <= 254) {
|
|
2236
|
-
// Application custom packet (toxcore lossless 160-191 / lossy
|
|
2237
|
-
// 192-254). SDK-reserved IDs (160 UDP_ENDPOINT, 254 NAT_PING, …)
|
|
2238
|
-
// are handled by earlier branches and never reach here; anything
|
|
2239
|
-
// else belongs to the application layer (e.g. decentlan IP traffic
|
|
2240
|
-
// on 192, control on 161/162). Receiving one proves the friend is
|
|
2241
|
-
// reachable, so mark online.
|
|
2242
|
-
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
2243
|
-
this.#events.emit("customPacket", {
|
|
2244
|
-
pubkey: friendId,
|
|
2245
|
-
id: kind,
|
|
2246
|
-
data: inner
|
|
2247
|
-
});
|
|
2248
|
-
return;
|
|
2101
|
+
else {
|
|
2102
|
+
this.#requestMissingReliablePackets(friendId, state);
|
|
2249
2103
|
}
|
|
2250
|
-
this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
|
|
2251
2104
|
return;
|
|
2252
2105
|
}
|
|
2253
2106
|
// Reverted: we used to DELETE a "desynced" session here to force a clean
|
|
@@ -3339,6 +3192,323 @@ export class Peer {
|
|
|
3339
3192
|
});
|
|
3340
3193
|
}
|
|
3341
3194
|
}
|
|
3195
|
+
/** Deliver ONE decrypted lossless payload to the application layer. Split
|
|
3196
|
+
* out of the datagram handler so the receive reorder buffer can replay
|
|
3197
|
+
* buffered packets through the exact same dispatch. `kind` is the first
|
|
3198
|
+
* payload byte (never undefined here — the caller defaults it). */
|
|
3199
|
+
#deliverLosslessPayload(friendId, state, kind, inner, remote) {
|
|
3200
|
+
// Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
|
|
3201
|
+
if (this.#fileTransfer.handlePacket(friendId, kind, inner))
|
|
3202
|
+
return;
|
|
3203
|
+
if (kind === PACKET_ID_ONLINE) {
|
|
3204
|
+
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
3205
|
+
this.#debugLog(`crypto online packet received from ${friendId}`);
|
|
3206
|
+
// First time the friend signals ONLINE, push our profile
|
|
3207
|
+
// (nickname + status message) and any configured greeting so the
|
|
3208
|
+
// other side updates "unknown" to a real name. Toxcore semantics:
|
|
3209
|
+
// these are sent on every reconnect by the friend_connection layer.
|
|
3210
|
+
this.#sendProfileAndGreeting(friendId);
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (kind === PACKET_ID_OFFLINE) {
|
|
3214
|
+
this.#setFriendOffline(friendId);
|
|
3215
|
+
this.#debugLog(`crypto offline packet received from ${friendId}`);
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
if (kind === PACKET_ID_KILL) {
|
|
3219
|
+
this.#friendSessions.delete(friendId);
|
|
3220
|
+
this.#setFriendOffline(friendId);
|
|
3221
|
+
this.#debugLog(`crypto kill received from ${friendId}, session torn down`);
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
if (kind === PACKET_ID_ALIVE) {
|
|
3225
|
+
// Pure liveness signal; lastPingRecvMs already updated above.
|
|
3226
|
+
this.#debugVerboseLog(`crypto alive (keepalive) received from ${friendId}`);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
if (kind === PACKET_ID_UDP_ENDPOINT) {
|
|
3230
|
+
// Peer told us their public UDP endpoint — hole-punch to it.
|
|
3231
|
+
this.#handleUdpEndpointOffer(friendId, inner);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
if (kind === PACKET_ID_REQUEST) {
|
|
3235
|
+
// Lossless retransmission request (toxcore handle_request_packet).
|
|
3236
|
+
// Bytes are 1-based deltas walking our send window from
|
|
3237
|
+
// sendBufferStartNum: a matching delta marks that packet number
|
|
3238
|
+
// REQUESTED (peer is missing it — resend); every walked number
|
|
3239
|
+
// that doesn't match is implicitly ACKED (peer has it — drop from
|
|
3240
|
+
// the buffer). 0 is a +255 filler.
|
|
3241
|
+
this.#handleRetransmitRequest(friendId, state, inner);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (kind === PACKET_ID_SHARE_RELAYS) {
|
|
3245
|
+
// Peer is sharing TCP relay endpoints they're connected to. We
|
|
3246
|
+
// don't have a TCP relay client yet, so cache nothing — just log.
|
|
3247
|
+
this.#debugVerboseLog(`crypto share-relays from ${friendId} (TCP relay client not implemented)`);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
if (kind === PACKET_ID_NICKNAME) {
|
|
3251
|
+
const name = decodeUtf8Best(inner);
|
|
3252
|
+
const friend = this.#friends.get(friendId);
|
|
3253
|
+
if (friend && name && friend.name !== name) {
|
|
3254
|
+
this.#friends.set(friendId, { ...friend, name });
|
|
3255
|
+
this.#persistFriends();
|
|
3256
|
+
this.#events.emit("friendInfo", {
|
|
3257
|
+
pubkey: friendId,
|
|
3258
|
+
userid: friend.userid ?? friendId,
|
|
3259
|
+
name,
|
|
3260
|
+
description: friend.description
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
this.#debugLog(`crypto nickname received from ${friendId}: "${name}"`);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (kind === PACKET_ID_STATUSMESSAGE) {
|
|
3267
|
+
// Carrier C SDK wraps the user-profile in a FlatBuffers
|
|
3268
|
+
// PACKET_TYPE_USERINFO packet here (name + descr + gender + phone
|
|
3269
|
+
// + email + region + has_avatar). Some non-Carrier toxcore peers
|
|
3270
|
+
// may still send raw UTF-8, so try the FB decode first and fall
|
|
3271
|
+
// back to plain UTF-8 for compatibility.
|
|
3272
|
+
let userInfoName;
|
|
3273
|
+
let userInfoDescr;
|
|
3274
|
+
let clientMeta;
|
|
3275
|
+
try {
|
|
3276
|
+
const decoded = decodeCarrierPacket(inner);
|
|
3277
|
+
if (decoded.type === PACKET_TYPE_USERINFO) {
|
|
3278
|
+
userInfoName = decoded.name;
|
|
3279
|
+
userInfoDescr = decoded.descr;
|
|
3280
|
+
// AgentNet appended fields — present only for updated peers.
|
|
3281
|
+
if (decoded.protoVersion || decoded.platform || decoded.appVersion) {
|
|
3282
|
+
clientMeta = {
|
|
3283
|
+
protoVersion: decoded.protoVersion,
|
|
3284
|
+
platform: decoded.platform,
|
|
3285
|
+
osVersion: decoded.osVersion,
|
|
3286
|
+
appVersion: decoded.appVersion
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
catch {
|
|
3292
|
+
// Not a Carrier userinfo packet — fall through.
|
|
3293
|
+
}
|
|
3294
|
+
if (userInfoName !== undefined || userInfoDescr !== undefined) {
|
|
3295
|
+
const friend = this.#friends.get(friendId);
|
|
3296
|
+
const newName = userInfoName && userInfoName.length > 0 ? userInfoName : friend?.name;
|
|
3297
|
+
const newDescr = userInfoDescr ?? friend?.description;
|
|
3298
|
+
const metaChanged = friend != null && clientMeta != null &&
|
|
3299
|
+
(friend.protoVersion !== clientMeta.protoVersion ||
|
|
3300
|
+
friend.platform !== clientMeta.platform ||
|
|
3301
|
+
friend.osVersion !== clientMeta.osVersion ||
|
|
3302
|
+
friend.appVersion !== clientMeta.appVersion);
|
|
3303
|
+
if (friend && (friend.name !== newName || friend.description !== newDescr || metaChanged)) {
|
|
3304
|
+
this.#friends.set(friendId, { ...friend, name: newName, description: newDescr, ...(clientMeta ?? {}) });
|
|
3305
|
+
this.#persistFriends();
|
|
3306
|
+
this.#events.emit("friendInfo", {
|
|
3307
|
+
pubkey: friendId,
|
|
3308
|
+
userid: friend.userid ?? friendId,
|
|
3309
|
+
name: newName,
|
|
3310
|
+
description: newDescr,
|
|
3311
|
+
...(clientMeta ?? {})
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
this.#debugLog(`crypto userinfo received from ${friendId}: name="${userInfoName ?? ""}" descr="${userInfoDescr ?? ""}"${clientMeta ? ` proto=${clientMeta.protoVersion} platform=${clientMeta.platform} app=${clientMeta.appVersion}` : ""}`);
|
|
3315
|
+
}
|
|
3316
|
+
else {
|
|
3317
|
+
const status = decodeUtf8Best(inner);
|
|
3318
|
+
const friend = this.#friends.get(friendId);
|
|
3319
|
+
if (friend && status && friend.description !== status) {
|
|
3320
|
+
this.#friends.set(friendId, { ...friend, description: status });
|
|
3321
|
+
this.#persistFriends();
|
|
3322
|
+
}
|
|
3323
|
+
this.#debugVerboseLog(`crypto status message (raw) received from ${friendId}: "${status}"`);
|
|
3324
|
+
}
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if (kind === PACKET_ID_USERSTATUS) {
|
|
3328
|
+
this.#debugVerboseLog(`crypto user-status received from ${friendId} (${inner[0] ?? "?"})`);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
if (kind === PACKET_ID_TYPING) {
|
|
3332
|
+
this.#debugVerboseLog(`crypto typing received from ${friendId} (${inner[0] ?? "?"})`);
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
if (kind === PACKET_ID_MESSAGE || kind === PACKET_ID_ACTION) {
|
|
3336
|
+
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
3337
|
+
// Decode the Carrier FlatBuffers wrapper when present. A BULKMSG
|
|
3338
|
+
// fragment (Carrier C splits any message >1024 bytes this way —
|
|
3339
|
+
// e.g. iOS Beagle images, which are FileModel JSON envelopes)
|
|
3340
|
+
// goes to the reassembler and only emits once complete; before
|
|
3341
|
+
// this, every fragment surfaced as a separate garbage "text".
|
|
3342
|
+
let text;
|
|
3343
|
+
let carrier;
|
|
3344
|
+
try {
|
|
3345
|
+
carrier = decodeCarrierPacket(inner);
|
|
3346
|
+
}
|
|
3347
|
+
catch { /* raw utf-8 peer */ }
|
|
3348
|
+
// Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST/RESPONSE = 34/35)
|
|
3349
|
+
// rides this same PACKET_ID_MESSAGE channel — it's what the iOS/Android
|
|
3350
|
+
// WebRTC SDK uses for call signaling via the "carrier" extension. Large
|
|
3351
|
+
// signals (SDP offers) fragment by tid like bulkmsg; reassemble then
|
|
3352
|
+
// surface as an "invite" event (NOT chat text).
|
|
3353
|
+
if (carrier?.type === PACKET_TYPE_INVITE_REQUEST) {
|
|
3354
|
+
const complete = this.#assembleInvite(friendId, carrier);
|
|
3355
|
+
if (!complete)
|
|
3356
|
+
return; // more fragments pending
|
|
3357
|
+
this.#events.emit("invite", {
|
|
3358
|
+
pubkey: friendId,
|
|
3359
|
+
ext: carrier.ext,
|
|
3360
|
+
bundle: carrier.bundle,
|
|
3361
|
+
data: complete
|
|
3362
|
+
});
|
|
3363
|
+
this.#debugLog(`invite from ${friendId}: ext="${carrier.ext ?? ""}" (${complete.length} bytes)`);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (carrier?.type === PACKET_TYPE_INVITE_RESPONSE) {
|
|
3367
|
+
const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier) : new Uint8Array();
|
|
3368
|
+
if (carrier.status === 0 && !complete)
|
|
3369
|
+
return; // more fragments pending
|
|
3370
|
+
this.#events.emit("inviteResponse", {
|
|
3371
|
+
pubkey: friendId,
|
|
3372
|
+
ext: carrier.ext,
|
|
3373
|
+
bundle: carrier.bundle,
|
|
3374
|
+
status: carrier.status,
|
|
3375
|
+
reason: carrier.reason,
|
|
3376
|
+
data: complete ?? new Uint8Array()
|
|
3377
|
+
});
|
|
3378
|
+
this.#debugLog(`invite-response from ${friendId}: status=${carrier.status} ext="${carrier.ext ?? ""}"`);
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
if (carrier?.type === PACKET_TYPE_BULKMSG) {
|
|
3382
|
+
const complete = this.#assembleBulkMsg(friendId, carrier);
|
|
3383
|
+
if (!complete)
|
|
3384
|
+
return; // more fragments pending
|
|
3385
|
+
text = decodeUtf8Best(complete);
|
|
3386
|
+
this.#debugLog(`bulkmsg complete from ${friendId} (${complete.length} bytes)`);
|
|
3387
|
+
}
|
|
3388
|
+
else if (carrier?.type === PACKET_TYPE_MESSAGE) {
|
|
3389
|
+
text = new TextDecoder().decode(carrier.data);
|
|
3390
|
+
}
|
|
3391
|
+
else {
|
|
3392
|
+
text = decodeUtf8Best(inner);
|
|
3393
|
+
}
|
|
3394
|
+
// Carrier C peers terminate strings with a NUL — strip it so chat
|
|
3395
|
+
// text doesn't carry an invisible trailing byte.
|
|
3396
|
+
text = text?.replace(/\0+$/u, "");
|
|
3397
|
+
if (!text) {
|
|
3398
|
+
this.#debugLog(`crypto message packet decode failed from ${friendId}`);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
// iOS Beagle sends files inline as a FileModel JSON envelope
|
|
3402
|
+
// ({data: base64, fileExtension, fileName, type}) — surface those
|
|
3403
|
+
// as files, not as a wall of base64 text.
|
|
3404
|
+
if (this.#tryEmitInlineFile(friendId, text, "online"))
|
|
3405
|
+
return;
|
|
3406
|
+
this.#events.emit("text", {
|
|
3407
|
+
pubkey: friendId,
|
|
3408
|
+
text,
|
|
3409
|
+
via: "online"
|
|
3410
|
+
});
|
|
3411
|
+
this.#debugLog(`crypto ${kind === PACKET_ID_ACTION ? "action" : "message"} from ${friendId}: "${text}"`);
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
if (kind >= 160 && kind <= 254) {
|
|
3415
|
+
// Application custom packet (toxcore lossless 160-191 / lossy
|
|
3416
|
+
// 192-254). SDK-reserved IDs (160 UDP_ENDPOINT, 254 NAT_PING, …)
|
|
3417
|
+
// are handled by earlier branches and never reach here; anything
|
|
3418
|
+
// else belongs to the application layer (e.g. decentlan IP traffic
|
|
3419
|
+
// on 192, control on 161/162). Receiving one proves the friend is
|
|
3420
|
+
// reachable, so mark online.
|
|
3421
|
+
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
3422
|
+
this.#events.emit("customPacket", {
|
|
3423
|
+
pubkey: friendId,
|
|
3424
|
+
id: kind,
|
|
3425
|
+
data: inner
|
|
3426
|
+
});
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
|
|
3430
|
+
}
|
|
3431
|
+
/** Drain the receive reorder buffer forward from receiveBufferStart while it
|
|
3432
|
+
* holds the next contiguous packet, delivering each in strict order. */
|
|
3433
|
+
#drainRecvBufferContiguous(friendId, state, remote) {
|
|
3434
|
+
const buf = state.recvBuffer;
|
|
3435
|
+
if (!buf)
|
|
3436
|
+
return;
|
|
3437
|
+
for (let held = buf.get(state.receiveBufferStart ?? 0); held; held = buf.get(state.receiveBufferStart ?? 0)) {
|
|
3438
|
+
buf.delete(state.receiveBufferStart ?? 0);
|
|
3439
|
+
state.receiveBufferStart = ((state.receiveBufferStart ?? 0) + 1) >>> 0;
|
|
3440
|
+
this.#deliverLosslessPayload(friendId, state, held.kind, held.inner, remote);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
/** Safety valve: the reorder buffer grew past the window, so a packet below
|
|
3444
|
+
* it is (almost certainly) a genuine loss that will never retransmit. Jump
|
|
3445
|
+
* the low-water mark to the lowest buffered number — abandoning the gap
|
|
3446
|
+
* (partial messages are dropped gracefully by the reassemblers) — rather
|
|
3447
|
+
* than wedging every later packet (incl. chat) forever. */
|
|
3448
|
+
#forceAdvanceRecvBuffer(friendId, state, remote) {
|
|
3449
|
+
const buf = state.recvBuffer;
|
|
3450
|
+
if (!buf || buf.size === 0)
|
|
3451
|
+
return;
|
|
3452
|
+
const expected = state.receiveBufferStart ?? 0;
|
|
3453
|
+
let lowest = expected;
|
|
3454
|
+
let bestDist = Infinity;
|
|
3455
|
+
for (const n of buf.keys()) {
|
|
3456
|
+
const d = (n - expected) >>> 0;
|
|
3457
|
+
if (d < bestDist) {
|
|
3458
|
+
bestDist = d;
|
|
3459
|
+
lowest = n;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
this.#debugLog(`recv reorder overflow from ${friendId}: abandoning gap [${expected}..${lowest}), ${buf.size} held`);
|
|
3463
|
+
state.receiveBufferStart = lowest;
|
|
3464
|
+
this.#drainRecvBufferContiguous(friendId, state, remote);
|
|
3465
|
+
}
|
|
3466
|
+
/** Ask the sender to retransmit the packets missing below our highest
|
|
3467
|
+
* buffered number. Encodes the toxcore PACKET_ID_REQUEST byte stream
|
|
3468
|
+
* (the exact inverse of #handleRetransmitRequest): walk [low, high] and
|
|
3469
|
+
* emit the running gap-counter at each MISSING number. Rate-limited. */
|
|
3470
|
+
#requestMissingReliablePackets(friendId, state) {
|
|
3471
|
+
const buf = state.recvBuffer;
|
|
3472
|
+
if (!buf || buf.size === 0)
|
|
3473
|
+
return;
|
|
3474
|
+
const now = Date.now();
|
|
3475
|
+
if (state.lastRecvRequestMs && now - state.lastRecvRequestMs < RECV_REQUEST_MIN_INTERVAL_MS)
|
|
3476
|
+
return;
|
|
3477
|
+
const low = state.receiveBufferStart ?? 0;
|
|
3478
|
+
let high = low;
|
|
3479
|
+
let bestDist = -1;
|
|
3480
|
+
for (const n of buf.keys()) {
|
|
3481
|
+
const d = (n - low) >>> 0;
|
|
3482
|
+
if (d > bestDist) {
|
|
3483
|
+
bestDist = d;
|
|
3484
|
+
high = n;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
const bytes = encodeRetransmitRequest(low, high, (n) => buf.has(n));
|
|
3488
|
+
if (bytes.length === 0)
|
|
3489
|
+
return;
|
|
3490
|
+
state.lastRecvRequestMs = now;
|
|
3491
|
+
void this.#sendRequestPacket(friendId, state, bytes).catch(() => { });
|
|
3492
|
+
}
|
|
3493
|
+
/** Send a PACKET_ID_REQUEST. Like a retransmit (#resendReliablePacket) it
|
|
3494
|
+
* rides the CURRENT nonce but does NOT consume a reliable send number —
|
|
3495
|
+
* toxcore sends requests at buffer_end without storing them. */
|
|
3496
|
+
async #sendRequestPacket(friendId, state, requestBytes) {
|
|
3497
|
+
if (!state.established || !state.sessionSharedKey || !state.ourBaseNonce)
|
|
3498
|
+
return;
|
|
3499
|
+
if (!state.remote && !state.hasTcpRoute)
|
|
3500
|
+
return;
|
|
3501
|
+
const payload = concatBytes([Uint8Array.of(PACKET_ID_REQUEST), requestBytes]);
|
|
3502
|
+
const encrypted = createCryptoDataPacket({
|
|
3503
|
+
sessionSharedKey: state.sessionSharedKey,
|
|
3504
|
+
sentNonce: state.ourBaseNonce,
|
|
3505
|
+
bufferStart: state.receiveBufferStart ?? 0,
|
|
3506
|
+
packetNumber: state.sendPacketNumber ?? 0,
|
|
3507
|
+
payload
|
|
3508
|
+
});
|
|
3509
|
+
incrementNonce(state.ourBaseNonce);
|
|
3510
|
+
await this.#sendToFriend(friendId, encrypted, state, false, false);
|
|
3511
|
+
}
|
|
3342
3512
|
/** Append a Carrier BULKMSG fragment; returns the full reassembled
|
|
3343
3513
|
* message when the last fragment lands, undefined while pending.
|
|
3344
3514
|
* Mirrors the C SDK's handle_friend_bulkmsg: totalsz on the first
|
|
@@ -3552,8 +3722,11 @@ export class Peer {
|
|
|
3552
3722
|
session.sendBufferStartNum ??= packetNumber;
|
|
3553
3723
|
session.sendArray.set(packetNumber, payload);
|
|
3554
3724
|
// Bound the buffer: beyond this window the peer has either acked via
|
|
3555
|
-
// REQUEST packets or the session is beyond saving anyway.
|
|
3556
|
-
|
|
3725
|
+
// REQUEST packets or the session is beyond saving anyway. Sized to match
|
|
3726
|
+
// the receive reorder window (8192) so a large multi-fragment bulk send
|
|
3727
|
+
// (16 MB inline file ≈ 16k fragments) can retransmit any fragment the peer
|
|
3728
|
+
// is still missing within the window rather than evicting it too early.
|
|
3729
|
+
while (session.sendArray.size > 8192 && session.sendBufferStartNum !== session.sendPacketNumber) {
|
|
3557
3730
|
session.sendArray.delete(session.sendBufferStartNum);
|
|
3558
3731
|
session.sendBufferStartNum = (session.sendBufferStartNum + 1) >>> 0;
|
|
3559
3732
|
}
|
|
@@ -3765,10 +3938,17 @@ export class Peer {
|
|
|
3765
3938
|
await sleep(250);
|
|
3766
3939
|
const userInfo = encodeUserInfoPacket({
|
|
3767
3940
|
name: nick,
|
|
3768
|
-
descr
|
|
3941
|
+
descr,
|
|
3942
|
+
// AgentNet client metadata (appended userinfo fields 7-10). Old
|
|
3943
|
+
// peers ignore them; updated peers negotiate capabilities + display
|
|
3944
|
+
// the friend's platform/build.
|
|
3945
|
+
protoVersion: AGENTNET_PROTO_VERSION,
|
|
3946
|
+
platform: this.#opts.platform ?? process.platform,
|
|
3947
|
+
osVersion: this.#opts.osVersion ?? `node-${process.versions?.node ?? ""}`,
|
|
3948
|
+
appVersion: this.#opts.appVersion ?? `peer-${PEER_PKG_VERSION}`
|
|
3769
3949
|
});
|
|
3770
3950
|
await this.#sendMessengerPacket(friendId, PACKET_ID_STATUSMESSAGE, userInfo);
|
|
3771
|
-
this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}")`);
|
|
3951
|
+
this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}", proto=${AGENTNET_PROTO_VERSION}, platform=${this.#opts.platform ?? process.platform})`);
|
|
3772
3952
|
}
|
|
3773
3953
|
catch (error) {
|
|
3774
3954
|
this.#debugLog(`send profile failed for ${friendId}: ${error.message}`);
|
|
@@ -5279,7 +5459,11 @@ export class Peer {
|
|
|
5279
5459
|
nodeDPort: nodeD.port,
|
|
5280
5460
|
payloadForNodeD
|
|
5281
5461
|
});
|
|
5282
|
-
this.#tcpRelays.sendOnionRequest(tcpPacket);
|
|
5462
|
+
const sent = this.#tcpRelays.sendOnionRequest(tcpPacket);
|
|
5463
|
+
if (sent > 0) {
|
|
5464
|
+
this.#diagTcpOnionSent += 1;
|
|
5465
|
+
this.#debugVerboseLog(`tcp onion request sent via ${sent} relay(s) to ${nodeD.host}:${nodeD.port} (B=${path.nodeB.host} C=${path.nodeC.host})`);
|
|
5466
|
+
}
|
|
5283
5467
|
}
|
|
5284
5468
|
catch {
|
|
5285
5469
|
/* malformed path node key — the UDP attempt above still stands */
|
package/dist/store/friends.d.ts
CHANGED
|
@@ -19,4 +19,12 @@ export type FriendRecord = {
|
|
|
19
19
|
* can immediately park relay ROUTING_REQUESTs under the right key
|
|
20
20
|
* instead of waiting for the friend's next DHT-PK announce. */
|
|
21
21
|
dhtPubkey?: string;
|
|
22
|
+
/** Client metadata the friend advertised in its userinfo profile. Lets the
|
|
23
|
+
* app negotiate capabilities (e.g. large-file transfer support) and show
|
|
24
|
+
* the peer's platform/build. Undefined / protoVersion 0 for a legacy peer
|
|
25
|
+
* that predates the userinfo extension. */
|
|
26
|
+
protoVersion?: number;
|
|
27
|
+
platform?: string;
|
|
28
|
+
osVersion?: string;
|
|
29
|
+
appVersion?: string;
|
|
22
30
|
};
|
package/dist/types/peer.d.ts
CHANGED
|
@@ -68,6 +68,20 @@ export type PeerOptions = {
|
|
|
68
68
|
nickname?: string;
|
|
69
69
|
/** Status/description line shown alongside the nickname. */
|
|
70
70
|
statusMessage?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Client platform advertised to friends in the userinfo profile — e.g.
|
|
73
|
+
* "js", "darwin", "linux", "ios", "android". Defaults to the Node
|
|
74
|
+
* `process.platform` value. Native Beagle apps advertise "ios" / "android".
|
|
75
|
+
*/
|
|
76
|
+
platform?: string;
|
|
77
|
+
/** OS/runtime version advertised in the profile (e.g. "node-24.2.0"). */
|
|
78
|
+
osVersion?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Application/SDK version advertised in the profile (e.g. "lan-0.1.167").
|
|
81
|
+
* Lets peers see which build a friend is on. Defaults to the peer package
|
|
82
|
+
* version.
|
|
83
|
+
*/
|
|
84
|
+
appVersion?: string;
|
|
71
85
|
compatibilityMode?: CompatibilityMode;
|
|
72
86
|
debugLabel?: string;
|
|
73
87
|
};
|
|
@@ -90,6 +104,14 @@ export type FriendInfoEvent = {
|
|
|
90
104
|
userid?: string;
|
|
91
105
|
name?: string;
|
|
92
106
|
description?: string;
|
|
107
|
+
/** Peer's advertised client metadata (from the userinfo profile). Present
|
|
108
|
+
* only once the friend has sent a profile with these fields — a legacy
|
|
109
|
+
* peer (native C SDK before the extension, or an old JS build) leaves them
|
|
110
|
+
* undefined / protoVersion 0. */
|
|
111
|
+
protoVersion?: number;
|
|
112
|
+
platform?: string;
|
|
113
|
+
osVersion?: string;
|
|
114
|
+
appVersion?: string;
|
|
93
115
|
};
|
|
94
116
|
export type TextMessage = {
|
|
95
117
|
pubkey: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
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",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"demo:tox-dht-crypto": "pnpm run build && node scripts/tox-dht-crypto-demo.mjs",
|
|
69
69
|
"test:compat": "pnpm run build && node scripts/compat-selftest.mjs",
|
|
70
70
|
"test:invite": "pnpm run build && node scripts/invite-selftest.mjs",
|
|
71
|
+
"test:reorder": "pnpm run build && node scripts/reorder-selftest.mjs",
|
|
72
|
+
"test:userinfo": "pnpm run build && node scripts/userinfo-selftest.mjs",
|
|
71
73
|
"test:tcp-onion": "pnpm run build && node scripts/tcp-onion-selftest.mjs",
|
|
72
74
|
"smoke:join": "node scripts/smoke-join.mjs",
|
|
73
75
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|