@decentnetwork/peer 0.1.79 → 0.1.82
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/express.js +3 -1
- package/dist/compat/net-crypto.d.ts +4 -0
- package/dist/compat/net-crypto.js +41 -3
- package/dist/compat/packet.d.ts +24 -1
- package/dist/compat/packet.js +48 -0
- package/dist/compat/tcp-relay-pool.js +11 -1
- package/dist/peer.d.ts +18 -1
- package/dist/peer.js +691 -97
- package/dist/store/friends.d.ts +5 -0
- package/dist/types/peer.d.ts +27 -0
- package/package.json +3 -2
package/dist/compat/express.js
CHANGED
|
@@ -28,6 +28,7 @@ export class LegacyExpressClient {
|
|
|
28
28
|
return {
|
|
29
29
|
host: node.host,
|
|
30
30
|
port: node.port,
|
|
31
|
+
tls: node.tls !== false, // default HTTPS; opt into HTTP with tls:false
|
|
31
32
|
sharedKey: nacl.box.before(expressPk, this.#selfKeyPair.secretKey)
|
|
32
33
|
};
|
|
33
34
|
});
|
|
@@ -152,7 +153,8 @@ export class LegacyExpressClient {
|
|
|
152
153
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
153
154
|
}
|
|
154
155
|
async #http(node, method, path, body) {
|
|
155
|
-
const
|
|
156
|
+
const scheme = node.tls ? "https" : "http";
|
|
157
|
+
const url = `${scheme}://${node.host}:${node.port}/${path}`;
|
|
156
158
|
const controller = new AbortController();
|
|
157
159
|
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
158
160
|
try {
|
|
@@ -80,5 +80,9 @@ export declare function openCryptoDataPacket(packet: Uint8Array, opts: {
|
|
|
80
80
|
bufferStart: number;
|
|
81
81
|
packetNumber: number;
|
|
82
82
|
nonceLast2: number;
|
|
83
|
+
/** The exact nonce this packet decrypted with. The caller must track
|
|
84
|
+
* recvBaseNonce = usedNonce (+1 for the next expected) — NOT overwrite
|
|
85
|
+
* just the low two bytes, which loses the carry into byte 21. */
|
|
86
|
+
usedNonce: Uint8Array;
|
|
83
87
|
} | undefined;
|
|
84
88
|
export declare function incrementNonce(nonce: Uint8Array): void;
|
|
@@ -191,9 +191,27 @@ export function openCryptoDataPacket(packet, opts) {
|
|
|
191
191
|
if (packet.length <= CRYPTO_DATA_PACKET_MIN_SIZE || packet[0] !== NET_PACKET_CRYPTO_DATA) {
|
|
192
192
|
return undefined;
|
|
193
193
|
}
|
|
194
|
+
// The wire carries only the low 16 bits of the sender's nonce counter.
|
|
195
|
+
// Reconstruct the full nonce like toxcore handle_data_packet: advance a
|
|
196
|
+
// COPY of our tracked nonce by the uint16 difference WITH CARRY into the
|
|
197
|
+
// upper bytes. The old code overwrote the two low bytes in place, which
|
|
198
|
+
// silently dropped the carry whenever packet loss spanned a 64k nonce
|
|
199
|
+
// wrap — from then on EVERY packet failed to decrypt until re-handshake
|
|
200
|
+
// (observed as endless "no session matched" from high-rate peers, whose
|
|
201
|
+
// counters wrap every ~30-60s of CCTV/file traffic).
|
|
194
202
|
const nonce = opts.recvBaseNonce.slice();
|
|
195
|
-
nonce[nonce.length - 2]
|
|
196
|
-
|
|
203
|
+
const trackedLow = ((nonce[nonce.length - 2] << 8) | nonce[nonce.length - 1]) >>> 0;
|
|
204
|
+
const wireLow = ((packet[1] << 8) | packet[2]) >>> 0;
|
|
205
|
+
const diff = (wireLow - trackedLow) & 0xffff;
|
|
206
|
+
if (diff < 0x8000) {
|
|
207
|
+
addToNonceCounter(nonce, diff);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// The wire counter is BEHIND our tracked one — a reordered/duplicate
|
|
211
|
+
// packet from earlier in the stream (dual-path UDP+relay delivery
|
|
212
|
+
// reorders constantly). Step backward with borrow.
|
|
213
|
+
subFromNonceCounter(nonce, 0x10000 - diff);
|
|
214
|
+
}
|
|
197
215
|
const cipher = packet.slice(3);
|
|
198
216
|
const plain = nacl.secretbox.open(cipher, nonce, opts.sessionSharedKey);
|
|
199
217
|
if (!plain || plain.length <= CRYPTO_DATA_PLAIN_HEADER_LENGTH) {
|
|
@@ -212,9 +230,29 @@ export function openCryptoDataPacket(packet, opts) {
|
|
|
212
230
|
payload: plain.slice(payloadOffset),
|
|
213
231
|
bufferStart,
|
|
214
232
|
packetNumber,
|
|
215
|
-
nonceLast2: ((packet[1] << 8) | packet[2]) >>> 0
|
|
233
|
+
nonceLast2: ((packet[1] << 8) | packet[2]) >>> 0,
|
|
234
|
+
usedNonce: nonce
|
|
216
235
|
};
|
|
217
236
|
}
|
|
237
|
+
/** Add `count` to the big-endian nonce counter with carry (toxcore
|
|
238
|
+
* increment_nonce_number). */
|
|
239
|
+
function addToNonceCounter(nonce, count) {
|
|
240
|
+
let carry = count >>> 0;
|
|
241
|
+
for (let i = nonce.length - 1; i >= 0 && carry > 0; i--) {
|
|
242
|
+
const sum = nonce[i] + (carry & 0xff);
|
|
243
|
+
nonce[i] = sum & 0xff;
|
|
244
|
+
carry = (carry >>> 8) + (sum > 0xff ? 1 : 0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Subtract `count` from the big-endian nonce counter with borrow. */
|
|
248
|
+
function subFromNonceCounter(nonce, count) {
|
|
249
|
+
let borrow = count >>> 0;
|
|
250
|
+
for (let i = nonce.length - 1; i >= 0 && borrow > 0; i--) {
|
|
251
|
+
const sub = nonce[i] - (borrow & 0xff);
|
|
252
|
+
nonce[i] = sub & 0xff;
|
|
253
|
+
borrow = (borrow >>> 8) + (sub < 0 ? 1 : 0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
218
256
|
function createCookie(opts) {
|
|
219
257
|
const timestamp = BigInt(Math.floor(Date.now() / 1000));
|
|
220
258
|
const contents = concatBytes([
|
package/dist/compat/packet.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const PACKET_TYPE_USERINFO = 3;
|
|
2
2
|
export declare const PACKET_TYPE_FRIEND_REQUEST = 6;
|
|
3
3
|
export declare const PACKET_TYPE_MESSAGE = 33;
|
|
4
|
+
export declare const PACKET_TYPE_BULKMSG = 36;
|
|
5
|
+
export declare const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
|
|
6
|
+
export declare const CARRIER_MAX_APP_BULKMSG_LEN: number;
|
|
4
7
|
export type UserInfoPacket = {
|
|
5
8
|
type: typeof PACKET_TYPE_USERINFO;
|
|
6
9
|
avatar: boolean;
|
|
@@ -22,7 +25,16 @@ export type FriendMessagePacket = {
|
|
|
22
25
|
ext?: string;
|
|
23
26
|
data: Uint8Array;
|
|
24
27
|
};
|
|
25
|
-
export type
|
|
28
|
+
export type BulkMsgPacket = {
|
|
29
|
+
type: typeof PACKET_TYPE_BULKMSG;
|
|
30
|
+
ext?: string;
|
|
31
|
+
/** Full message length — set on the FIRST fragment only, 0 on the rest. */
|
|
32
|
+
totalsz: number;
|
|
33
|
+
/** Shared id linking all fragments of one message. */
|
|
34
|
+
tid: bigint;
|
|
35
|
+
data: Uint8Array;
|
|
36
|
+
};
|
|
37
|
+
export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessagePacket | BulkMsgPacket;
|
|
26
38
|
/**
|
|
27
39
|
* Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
|
|
28
40
|
* this every time the local user's name / status message / profile fields
|
|
@@ -52,4 +64,15 @@ export declare function encodeFriendRequestPacket(opts: {
|
|
|
52
64
|
hello?: string;
|
|
53
65
|
}): Uint8Array;
|
|
54
66
|
export declare function encodeFriendMessagePacket(data: Uint8Array | string, ext?: string): Uint8Array;
|
|
67
|
+
/**
|
|
68
|
+
* Encode one Carrier `bulkmsg` fragment (PACKET_TYPE_BULKMSG = 36). Field
|
|
69
|
+
* layout per src/carrier/packet.fbs: 0=ext string, 1=totalsz uint,
|
|
70
|
+
* 2=tid long, 3=data [ubyte]. Mirrors the C SDK's send_bulk_message.
|
|
71
|
+
*/
|
|
72
|
+
export declare function encodeBulkMsgPacket(opts: {
|
|
73
|
+
ext?: string;
|
|
74
|
+
totalsz: number;
|
|
75
|
+
tid: bigint;
|
|
76
|
+
data: Uint8Array;
|
|
77
|
+
}): Uint8Array;
|
|
55
78
|
export declare function decodeCarrierPacket(bytes: Uint8Array): CarrierPacket;
|
package/dist/compat/packet.js
CHANGED
|
@@ -2,12 +2,20 @@ import { Builder, ByteBuffer, Encoding } from "flatbuffers";
|
|
|
2
2
|
export const PACKET_TYPE_USERINFO = 3;
|
|
3
3
|
export const PACKET_TYPE_FRIEND_REQUEST = 6;
|
|
4
4
|
export const PACKET_TYPE_MESSAGE = 33;
|
|
5
|
+
export const PACKET_TYPE_BULKMSG = 36;
|
|
6
|
+
// Carrier C SDK message-size constants (carrier.h). A single friendmsg may
|
|
7
|
+
// carry at most 1024 bytes; anything larger is split into BULKMSG fragments
|
|
8
|
+
// of <=1024 raw bytes sharing one tid (totalsz set on the FIRST fragment
|
|
9
|
+
// only), reassembled in arrival order by the receiver.
|
|
10
|
+
export const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
|
|
11
|
+
export const CARRIER_MAX_APP_BULKMSG_LEN = 5 * 1024 * 1024;
|
|
5
12
|
// Body type indices into FlatBuffers `anybody` union (carrier/packet.fbs).
|
|
6
13
|
// The .fbs declares: userinfo, friendreq, friendmsg, invitereq, invitersp,
|
|
7
14
|
// bulkmsg — flatbuffers maps those to 1, 2, 3, 4, 5, 6 (NONE = 0).
|
|
8
15
|
const ANYBODY_USERINFO = 1;
|
|
9
16
|
const ANYBODY_FRIENDREQ = 2;
|
|
10
17
|
const ANYBODY_FRIENDMSG = 3;
|
|
18
|
+
const ANYBODY_BULKMSG = 6;
|
|
11
19
|
/**
|
|
12
20
|
* Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
|
|
13
21
|
* this every time the local user's name / status message / profile fields
|
|
@@ -69,6 +77,29 @@ export function encodeFriendMessagePacket(data, ext) {
|
|
|
69
77
|
const friendMsg = builder.endObject();
|
|
70
78
|
return finishCarrierPacket(builder, PACKET_TYPE_MESSAGE, ANYBODY_FRIENDMSG, friendMsg);
|
|
71
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Encode one Carrier `bulkmsg` fragment (PACKET_TYPE_BULKMSG = 36). Field
|
|
82
|
+
* layout per src/carrier/packet.fbs: 0=ext string, 1=totalsz uint,
|
|
83
|
+
* 2=tid long, 3=data [ubyte]. Mirrors the C SDK's send_bulk_message.
|
|
84
|
+
*/
|
|
85
|
+
export function encodeBulkMsgPacket(opts) {
|
|
86
|
+
const builder = new Builder(opts.data.length + 128);
|
|
87
|
+
const dataOffset = builder.createByteVector(opts.data);
|
|
88
|
+
const extOffset = opts.ext ? builder.createString(opts.ext) : 0;
|
|
89
|
+
builder.startObject(4);
|
|
90
|
+
builder.addFieldOffset(3, dataOffset, 0);
|
|
91
|
+
if (opts.tid !== 0n) {
|
|
92
|
+
builder.addFieldInt64(2, opts.tid, BigInt(0));
|
|
93
|
+
}
|
|
94
|
+
if (opts.totalsz) {
|
|
95
|
+
builder.addFieldInt32(1, opts.totalsz, 0);
|
|
96
|
+
}
|
|
97
|
+
if (extOffset) {
|
|
98
|
+
builder.addFieldOffset(0, extOffset, 0);
|
|
99
|
+
}
|
|
100
|
+
const bulk = builder.endObject();
|
|
101
|
+
return finishCarrierPacket(builder, PACKET_TYPE_BULKMSG, ANYBODY_BULKMSG, bulk);
|
|
102
|
+
}
|
|
72
103
|
export function decodeCarrierPacket(bytes) {
|
|
73
104
|
const bb = new ByteBuffer(bytes);
|
|
74
105
|
const root = bb.readInt32(bb.position()) + bb.position();
|
|
@@ -106,6 +137,15 @@ export function decodeCarrierPacket(bytes) {
|
|
|
106
137
|
data: readByteVectorField(body, 1) ?? new Uint8Array()
|
|
107
138
|
};
|
|
108
139
|
}
|
|
140
|
+
if (type === PACKET_TYPE_BULKMSG && bodyType === ANYBODY_BULKMSG) {
|
|
141
|
+
return {
|
|
142
|
+
type,
|
|
143
|
+
ext: readStringField(body, 0) ?? undefined,
|
|
144
|
+
totalsz: readUint32Field(body, 1),
|
|
145
|
+
tid: readInt64Field(body, 2),
|
|
146
|
+
data: readByteVectorField(body, 3) ?? new Uint8Array()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
109
149
|
throw new Error(`unsupported carrier packet type/body: ${type}/${bodyType}`);
|
|
110
150
|
}
|
|
111
151
|
function finishCarrierPacket(builder, type, bodyType, bodyOffset) {
|
|
@@ -125,6 +165,14 @@ function readUint8Field(table, field) {
|
|
|
125
165
|
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
126
166
|
return offset ? table.bb.readUint8(table.bb_pos + offset) : 0;
|
|
127
167
|
}
|
|
168
|
+
function readUint32Field(table, field) {
|
|
169
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
170
|
+
return offset ? table.bb.readUint32(table.bb_pos + offset) : 0;
|
|
171
|
+
}
|
|
172
|
+
function readInt64Field(table, field) {
|
|
173
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
174
|
+
return offset ? table.bb.readInt64(table.bb_pos + offset) : BigInt(0);
|
|
175
|
+
}
|
|
128
176
|
function readStringField(table, field) {
|
|
129
177
|
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
130
178
|
if (!offset) {
|
|
@@ -23,7 +23,17 @@ import { EventEmitter } from "node:events";
|
|
|
23
23
|
import { base58ToBytes } from "../utils/base58.js";
|
|
24
24
|
import { TcpRelayClient } from "./tcp-relay.js";
|
|
25
25
|
const KEY_SIZE = 32;
|
|
26
|
-
|
|
26
|
+
// Connect to (up to) this many bootstrap nodes as persistent TCP relays. It was
|
|
27
|
+
// 3 — but the pool picks the FIRST N in order, so a peer only met friends whose
|
|
28
|
+
// relays happened to be in its own top-3. A NATIVE peer (iOS/Android) reaches us
|
|
29
|
+
// ONLY over a relay we both sit on (OOB cookie handshake); if our top-3 doesn't
|
|
30
|
+
// include the relay it uses, it's stuck "connecting" forever — even though we
|
|
31
|
+
// share the node further down the list (observed: iOS uses sh.callt.net =
|
|
32
|
+
// 47.100.103.201 as its #1 relay, which sat at #7 in our bootstrap so we never
|
|
33
|
+
// connected to it). Covering more of the shared bootstrap set makes the same-
|
|
34
|
+
// relay rendezvous reliable for native peers. Cheap: a handful of extra idle TCP
|
|
35
|
+
// keepalives.
|
|
36
|
+
const MAX_RELAY_CONNECTIONS = 8;
|
|
27
37
|
const RECONNECT_BASE_MS = 5_000;
|
|
28
38
|
const RECONNECT_MAX_MS = 120_000;
|
|
29
39
|
export class TcpRelayPool extends EventEmitter {
|
package/dist/peer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BootstrapResult } from "./compat/bootstrap.js";
|
|
2
2
|
import type { FriendRecord } from "./store/friends.js";
|
|
3
|
-
import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
|
3
|
+
import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
|
4
4
|
export declare class Peer {
|
|
5
5
|
#private;
|
|
6
6
|
private constructor();
|
|
@@ -82,6 +82,23 @@ export declare class Peer {
|
|
|
82
82
|
waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>;
|
|
83
83
|
onFriendRequest(cb: (req: FriendRequest) => void): void;
|
|
84
84
|
onText(cb: (msg: TextMessage) => void): void;
|
|
85
|
+
/** Files received inline over (bulk)messages — the iOS/C Carrier apps'
|
|
86
|
+
* native way of sending images/audio online (FileModel JSON envelope). */
|
|
87
|
+
onInlineFile(cb: (evt: InlineFileEvent) => void): void;
|
|
88
|
+
/**
|
|
89
|
+
* Send a file INLINE over the message channel the way iOS/C Carrier apps
|
|
90
|
+
* do: a FileModel JSON envelope ({data: base64, fileExtension, fileName,
|
|
91
|
+
* type}) carried as a (bulk)message. This is the ONLY file path a native
|
|
92
|
+
* Beagle client can receive online — the toxcore file transfer (80-82)
|
|
93
|
+
* used by sendFile() is invisible to the Carrier C SDK. Best for images
|
|
94
|
+
* and small files; the hard protocol cap is ~5MB (base64-inflated), keep
|
|
95
|
+
* real use well below it.
|
|
96
|
+
*/
|
|
97
|
+
sendInlineFile(pubkey: string, opts: {
|
|
98
|
+
name: string;
|
|
99
|
+
data: Uint8Array;
|
|
100
|
+
fileType?: "image" | "audio" | "text" | "unknown";
|
|
101
|
+
}): Promise<void>;
|
|
85
102
|
/**
|
|
86
103
|
* Send an application-defined custom packet to a friend. `id` selects the
|
|
87
104
|
* channel and its delivery class by toxcore range: **160–191 lossless**
|