@decentnetwork/peer 0.1.42 → 0.1.44

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.
@@ -0,0 +1,58 @@
1
+ export declare const PACKET_ID_FILE_SENDREQUEST = 80;
2
+ export declare const PACKET_ID_FILE_CONTROL = 81;
3
+ export declare const PACKET_ID_FILE_DATA = 82;
4
+ export declare const FILE_ID_LENGTH = 32;
5
+ export declare const MAX_FILENAME_LENGTH = 255;
6
+ export declare const MAX_CONCURRENT_FILE_PIPES = 256;
7
+ export declare const MAX_FILE_DATA_SIZE = 1371;
8
+ export declare const FILECONTROL: {
9
+ readonly ACCEPT: 0;
10
+ readonly PAUSE: 1;
11
+ readonly KILL: 2;
12
+ readonly SEEK: 3;
13
+ };
14
+ export declare const FILEKIND: {
15
+ readonly DATA: 0;
16
+ readonly AVATAR: 1;
17
+ };
18
+ export declare function encodeFileSendRequest(fileNumber: number, fileType: number, fileSize: bigint, fileId: Uint8Array, filename: string): Uint8Array;
19
+ export declare function parseFileSendRequest(p: Uint8Array): {
20
+ fileNumber: number;
21
+ fileType: number;
22
+ fileSize: bigint;
23
+ fileId: Uint8Array;
24
+ filename: string;
25
+ } | undefined;
26
+ export declare function encodeFileControl(sendReceive: number, fileNumber: number, controlType: number, data?: Uint8Array): Uint8Array;
27
+ export declare function parseFileControl(p: Uint8Array): {
28
+ sendReceive: number;
29
+ fileNumber: number;
30
+ controlType: number;
31
+ data: Uint8Array;
32
+ } | undefined;
33
+ export declare function encodeFileData(fileNumber: number, data: Uint8Array): Uint8Array;
34
+ export declare function parseFileData(p: Uint8Array): {
35
+ fileNumber: number;
36
+ data: Uint8Array;
37
+ } | undefined;
38
+ export type FtSend = (friendId: string, packetId: number, payload: Uint8Array) => Promise<unknown>;
39
+ export type FtEmit = (event: string, payload: Record<string, unknown>) => void;
40
+ export declare class FileTransferManager {
41
+ #private;
42
+ private readonly send;
43
+ private readonly emit;
44
+ constructor(send: FtSend, emit: FtEmit);
45
+ /** Offer a file to a friend. Returns the fileId (hex) or null if no slot. */
46
+ sendFile(friendId: string, data: Uint8Array, opts: {
47
+ name: string;
48
+ kind?: number;
49
+ }): string | null;
50
+ /** Accept an incoming offer (by the friend + the offered fileNumber). */
51
+ accept(friendId: string, fileNumber: number): void;
52
+ /** Cancel a transfer (sending or receiving). */
53
+ cancel(friendId: string, fileNumber: number, isSending: boolean): void;
54
+ /** Route an inbound messenger packet. Returns true if it was a file packet. */
55
+ handlePacket(friendId: string, packetId: number, payload: Uint8Array): boolean;
56
+ /** Drop all transfers for a friend (e.g. on disconnect). */
57
+ clearFriend(friendId: string): void;
58
+ }
@@ -0,0 +1,240 @@
1
+ // Toxcore-standard file transfer (Method "B") — wire-compatible with the file
2
+ // transfer already implemented in the C++ Carrier SDK's bundled toxcore
3
+ // (tox_file_send / file_recv*). Rides the messenger / net_crypto lossless
4
+ // channel; reassembled sequentially. No express, no 5 MB cap.
5
+ //
6
+ // Wire (messenger packet payloads):
7
+ // FILE_SENDREQUEST(80): [filenum(1)][file_type u32 BE][file_size u64 BE][file_id(32)][filename(≤255)]
8
+ // FILE_CONTROL(81): [send_receive(1)][filenum(1)][control_type(1)][optional data]
9
+ // FILE_DATA(82): [filenum(1)][data ≤ MAX_FILE_DATA_SIZE] (position implicit; empty = EOF)
10
+ // send_receive: 1 ⇒ filenum is in the RECIPIENT's *sending* table, 0 ⇒ *receiving*.
11
+ import { concatBytes, randomBytes } from "../utils/bytes.js";
12
+ export const PACKET_ID_FILE_SENDREQUEST = 80;
13
+ export const PACKET_ID_FILE_CONTROL = 81;
14
+ export const PACKET_ID_FILE_DATA = 82;
15
+ export const FILE_ID_LENGTH = 32;
16
+ export const MAX_FILENAME_LENGTH = 255;
17
+ export const MAX_CONCURRENT_FILE_PIPES = 256;
18
+ // MAX_CRYPTO_DATA_SIZE (1373) − 2; conservative so a chunk always fits one packet.
19
+ export const MAX_FILE_DATA_SIZE = 1371;
20
+ export const FILECONTROL = { ACCEPT: 0, PAUSE: 1, KILL: 2, SEEK: 3 };
21
+ export const FILEKIND = { DATA: 0, AVATAR: 1 };
22
+ function u32be(n) {
23
+ const b = new Uint8Array(4);
24
+ b[0] = (n >>> 24) & 0xff;
25
+ b[1] = (n >>> 16) & 0xff;
26
+ b[2] = (n >>> 8) & 0xff;
27
+ b[3] = n & 0xff;
28
+ return b;
29
+ }
30
+ function u64be(n) {
31
+ const b = new Uint8Array(8);
32
+ let v = n;
33
+ for (let i = 7; i >= 0; i--) {
34
+ b[i] = Number(v & 0xffn);
35
+ v >>= 8n;
36
+ }
37
+ return b;
38
+ }
39
+ function readU32be(b, o) {
40
+ return ((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0;
41
+ }
42
+ function readU64be(b, o) {
43
+ let v = 0n;
44
+ for (let i = 0; i < 8; i++)
45
+ v = (v << 8n) | BigInt(b[o + i]);
46
+ return v;
47
+ }
48
+ const enc = new TextEncoder();
49
+ const dec = new TextDecoder();
50
+ export function encodeFileSendRequest(fileNumber, fileType, fileSize, fileId, filename) {
51
+ const nameBytes = enc.encode(filename).slice(0, MAX_FILENAME_LENGTH);
52
+ return concatBytes([Uint8Array.of(fileNumber & 0xff), u32be(fileType), u64be(fileSize), fileId.slice(0, FILE_ID_LENGTH), nameBytes]);
53
+ }
54
+ export function parseFileSendRequest(p) {
55
+ if (p.length < 1 + 4 + 8 + FILE_ID_LENGTH)
56
+ return undefined;
57
+ return {
58
+ fileNumber: p[0],
59
+ fileType: readU32be(p, 1),
60
+ fileSize: readU64be(p, 5),
61
+ fileId: p.slice(13, 13 + FILE_ID_LENGTH),
62
+ filename: dec.decode(p.slice(13 + FILE_ID_LENGTH)),
63
+ };
64
+ }
65
+ export function encodeFileControl(sendReceive, fileNumber, controlType, data) {
66
+ const head = Uint8Array.of(sendReceive & 1, fileNumber & 0xff, controlType & 0xff);
67
+ return data && data.length ? concatBytes([head, data]) : head;
68
+ }
69
+ export function parseFileControl(p) {
70
+ if (p.length < 3)
71
+ return undefined;
72
+ return { sendReceive: p[0], fileNumber: p[1], controlType: p[2], data: p.slice(3) };
73
+ }
74
+ export function encodeFileData(fileNumber, data) {
75
+ return concatBytes([Uint8Array.of(fileNumber & 0xff), data]);
76
+ }
77
+ export function parseFileData(p) {
78
+ if (p.length < 1)
79
+ return undefined;
80
+ return { fileNumber: p[0], data: p.slice(1) };
81
+ }
82
+ const hex = (b) => Buffer.from(b).toString("hex");
83
+ export class FileTransferManager {
84
+ send;
85
+ emit;
86
+ // per friend → per fileNumber
87
+ #sending = new Map();
88
+ #receiving = new Map();
89
+ constructor(send, emit) {
90
+ this.send = send;
91
+ this.emit = emit;
92
+ }
93
+ #map(m, friendId) {
94
+ let inner = m.get(friendId);
95
+ if (!inner) {
96
+ inner = new Map();
97
+ m.set(friendId, inner);
98
+ }
99
+ return inner;
100
+ }
101
+ #freeNumber(friendId) {
102
+ const inner = this.#map(this.#sending, friendId);
103
+ for (let i = 0; i < MAX_CONCURRENT_FILE_PIPES; i++)
104
+ if (!inner.has(i))
105
+ return i;
106
+ return -1;
107
+ }
108
+ /** Offer a file to a friend. Returns the fileId (hex) or null if no slot. */
109
+ sendFile(friendId, data, opts) {
110
+ const fileNumber = this.#freeNumber(friendId);
111
+ if (fileNumber < 0)
112
+ return null;
113
+ const fileId = randomBytes(FILE_ID_LENGTH);
114
+ const st = { fileNumber, fileId, name: opts.name, data, size: data.length, transferred: 0, accepted: false, pumping: false };
115
+ this.#map(this.#sending, friendId).set(fileNumber, st);
116
+ const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
117
+ void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
118
+ return hex(fileId);
119
+ }
120
+ /** Accept an incoming offer (by the friend + the offered fileNumber). */
121
+ accept(friendId, fileNumber) {
122
+ const st = this.#map(this.#receiving, friendId).get(fileNumber);
123
+ if (!st || st.accepted)
124
+ return;
125
+ st.accepted = true;
126
+ // send_receive=1 → the filenumber lives in the SENDER's sending table.
127
+ void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(1, fileNumber, FILECONTROL.ACCEPT)).catch(() => undefined);
128
+ }
129
+ /** Cancel a transfer (sending or receiving). */
130
+ cancel(friendId, fileNumber, isSending) {
131
+ const sr = isSending ? 0 : 1;
132
+ void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(sr, fileNumber, FILECONTROL.KILL)).catch(() => undefined);
133
+ (isSending ? this.#map(this.#sending, friendId) : this.#map(this.#receiving, friendId)).delete(fileNumber);
134
+ }
135
+ /** Route an inbound messenger packet. Returns true if it was a file packet. */
136
+ handlePacket(friendId, packetId, payload) {
137
+ if (packetId === PACKET_ID_FILE_SENDREQUEST) {
138
+ this.#onSendRequest(friendId, payload);
139
+ return true;
140
+ }
141
+ if (packetId === PACKET_ID_FILE_CONTROL) {
142
+ this.#onControl(friendId, payload);
143
+ return true;
144
+ }
145
+ if (packetId === PACKET_ID_FILE_DATA) {
146
+ this.#onData(friendId, payload);
147
+ return true;
148
+ }
149
+ return false;
150
+ }
151
+ #onSendRequest(friendId, payload) {
152
+ const r = parseFileSendRequest(payload);
153
+ if (!r)
154
+ return;
155
+ const st = { fileNumber: r.fileNumber, fileId: r.fileId, name: r.filename, size: Number(r.fileSize), chunks: [], received: 0, accepted: false };
156
+ this.#map(this.#receiving, friendId).set(r.fileNumber, st);
157
+ this.emit("file-offer", { friendId, fileNumber: r.fileNumber, fileId: hex(r.fileId), name: r.filename, size: st.size, kind: r.fileType });
158
+ }
159
+ #onControl(friendId, payload) {
160
+ const c = parseFileControl(payload);
161
+ if (!c)
162
+ return;
163
+ // send_receive=1 → refers to a file WE are sending; 0 → a file we are receiving.
164
+ if (c.sendReceive === 1) {
165
+ const st = this.#map(this.#sending, friendId).get(c.fileNumber);
166
+ if (!st)
167
+ return;
168
+ if (c.controlType === FILECONTROL.ACCEPT) {
169
+ st.accepted = true;
170
+ void this.#pump(friendId, st);
171
+ }
172
+ else if (c.controlType === FILECONTROL.KILL) {
173
+ this.#map(this.#sending, friendId).delete(c.fileNumber);
174
+ this.emit("file-cancel", { friendId, fileId: hex(st.fileId), sending: true });
175
+ }
176
+ }
177
+ else {
178
+ const st = this.#map(this.#receiving, friendId).get(c.fileNumber);
179
+ if (!st)
180
+ return;
181
+ if (c.controlType === FILECONTROL.KILL) {
182
+ this.#map(this.#receiving, friendId).delete(c.fileNumber);
183
+ this.emit("file-cancel", { friendId, fileId: hex(st.fileId), sending: false });
184
+ }
185
+ }
186
+ }
187
+ #onData(friendId, payload) {
188
+ const d = parseFileData(payload);
189
+ if (!d)
190
+ return;
191
+ const st = this.#map(this.#receiving, friendId).get(d.fileNumber);
192
+ if (!st)
193
+ return;
194
+ if (d.data.length === 0 || (st.size !== 0 && st.received >= st.size)) {
195
+ // EOF (empty final chunk) or all bytes in.
196
+ const full = concatBytes(st.chunks);
197
+ this.#map(this.#receiving, friendId).delete(d.fileNumber);
198
+ this.emit("file-complete", { friendId, fileId: hex(st.fileId), name: st.name, size: st.size, data: full });
199
+ return;
200
+ }
201
+ st.chunks.push(d.data);
202
+ st.received += d.data.length;
203
+ this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.received, total: st.size });
204
+ if (st.size !== 0 && st.received >= st.size) {
205
+ const full = concatBytes(st.chunks);
206
+ this.#map(this.#receiving, friendId).delete(d.fileNumber);
207
+ this.emit("file-complete", { friendId, fileId: hex(st.fileId), name: st.name, size: st.size, data: full });
208
+ }
209
+ }
210
+ // Pump chunks over the lossless messenger channel. net_crypto applies its own
211
+ // congestion control, so awaiting each send gives natural backpressure.
212
+ async #pump(friendId, st) {
213
+ if (st.pumping)
214
+ return;
215
+ st.pumping = true;
216
+ try {
217
+ while (st.transferred < st.size) {
218
+ const end = Math.min(st.transferred + MAX_FILE_DATA_SIZE, st.size);
219
+ const chunk = st.data.subarray(st.transferred, end);
220
+ await this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, chunk));
221
+ st.transferred = end;
222
+ this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.transferred, total: st.size, sending: true });
223
+ if (!this.#map(this.#sending, friendId).has(st.fileNumber))
224
+ return; // cancelled
225
+ }
226
+ // Empty final chunk marks EOF for the receiver.
227
+ await this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, new Uint8Array(0)));
228
+ this.#map(this.#sending, friendId).delete(st.fileNumber);
229
+ this.emit("file-complete", { friendId, fileId: hex(st.fileId), name: st.name, size: st.size, sending: true });
230
+ }
231
+ catch {
232
+ this.#map(this.#sending, friendId).delete(st.fileNumber);
233
+ }
234
+ }
235
+ /** Drop all transfers for a friend (e.g. on disconnect). */
236
+ clearFriend(friendId) {
237
+ this.#sending.delete(friendId);
238
+ this.#receiving.delete(friendId);
239
+ }
240
+ }
package/dist/peer.d.ts CHANGED
@@ -90,6 +90,51 @@ export declare class Peer {
90
90
  * {@link sendCustomPacket}.
91
91
  */
92
92
  onCustomPacket(cb: (evt: CustomPacketEvent) => void): void;
93
+ /**
94
+ * Offer a file to a friend (by userid). Returns the fileId (hex) or null if
95
+ * the friend has no free transfer slot. The recipient gets an `onFile` offer;
96
+ * once they `acceptFile`, chunks stream over the reliable net_crypto channel.
97
+ */
98
+ sendFile(userid: string, data: Uint8Array, opts: {
99
+ name: string;
100
+ kind?: number;
101
+ }): string | null;
102
+ /** Accept an incoming file offer (the `fileNumber` from the onFile event). */
103
+ acceptFile(userid: string, fileNumber: number): void;
104
+ /** Cancel a transfer (isSending = true if we're the sender). */
105
+ cancelFile(userid: string, fileNumber: number, isSending?: boolean): void;
106
+ /** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
107
+ onFile(cb: (offer: {
108
+ friendId: string;
109
+ fileNumber: number;
110
+ fileId: string;
111
+ name: string;
112
+ size: number;
113
+ kind: number;
114
+ }) => void): void;
115
+ /** Transfer progress: { friendId, fileId, received, total, sending? }. */
116
+ onFileProgress(cb: (p: {
117
+ friendId: string;
118
+ fileId: string;
119
+ received: number;
120
+ total: number;
121
+ sending?: boolean;
122
+ }) => void): void;
123
+ /** Transfer complete: { friendId, fileId, name, size, data?, sending? } (data present on the receiver). */
124
+ onFileComplete(cb: (p: {
125
+ friendId: string;
126
+ fileId: string;
127
+ name: string;
128
+ size: number;
129
+ data?: Uint8Array;
130
+ sending?: boolean;
131
+ }) => void): void;
132
+ /** Transfer cancelled by the peer: { friendId, fileId, sending }. */
133
+ onFileCancel(cb: (p: {
134
+ friendId: string;
135
+ fileId: string;
136
+ sending: boolean;
137
+ }) => void): void;
93
138
  onFriendConnection(cb: (ev: FriendConnectionEvent) => void): void;
94
139
  onFriendInfo(cb: (ev: FriendInfoEvent) => void): void;
95
140
  friends(): FriendRecord[];
package/dist/peer.js CHANGED
@@ -9,6 +9,7 @@ import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO,
9
9
  import { NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID, createOnionAnnounceRequest, createOnionDataPacket, createOnionDataRequest, createOnionRequest0, 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";
12
+ import { FileTransferManager } from "./compat/filetransfer.js";
12
13
  import { NET_PACKET_CRYPTO_DATA, NET_PACKET_CRYPTO_HS, NET_PACKET_COOKIE_REQUEST, NET_PACKET_COOKIE_RESPONSE, createCookieRequest, createCookieResponse, createCryptoDataPacket, createCryptoHandshake, openCookieRequest, openCookieResponse, openCryptoDataPacket, openCryptoHandshake, incrementNonce } from "./compat/net-crypto.js";
13
14
  import { LegacyExpressClient } from "./compat/express.js";
14
15
  import { TcpRelayPool } from "./compat/tcp-relay-pool.js";
@@ -149,6 +150,8 @@ const PACKET_ID_UDP_ENDPOINT = 160;
149
150
  export class Peer {
150
151
  #opts;
151
152
  #events = new EventEmitter();
153
+ // Toxcore-standard file transfer (wire-compatible with native Carrier/toxcore).
154
+ #fileTransfer = new FileTransferManager((friendId, packetId, payload) => this.#sendMessengerPacket(friendId, packetId, payload), (event, payload) => { this.#events.emit(event, payload); });
152
155
  #keyPair;
153
156
  #udp = new UdpTransport();
154
157
  // TURN relay (stable fallback path). One node-level allocation on its own
@@ -945,6 +948,29 @@ export class Peer {
945
948
  onCustomPacket(cb) {
946
949
  this.#events.on("customPacket", cb);
947
950
  }
951
+ // ───────────────────── file transfer (toxcore-standard, native-ready) ─────
952
+ /**
953
+ * Offer a file to a friend (by userid). Returns the fileId (hex) or null if
954
+ * the friend has no free transfer slot. The recipient gets an `onFile` offer;
955
+ * once they `acceptFile`, chunks stream over the reliable net_crypto channel.
956
+ */
957
+ sendFile(userid, data, opts) {
958
+ if (!this.#friends.get(userid))
959
+ throw new Error(`Not a friend: ${userid}`);
960
+ return this.#fileTransfer.sendFile(userid, data, opts);
961
+ }
962
+ /** Accept an incoming file offer (the `fileNumber` from the onFile event). */
963
+ acceptFile(userid, fileNumber) { this.#fileTransfer.accept(userid, fileNumber); }
964
+ /** Cancel a transfer (isSending = true if we're the sender). */
965
+ cancelFile(userid, fileNumber, isSending = false) { this.#fileTransfer.cancel(userid, fileNumber, isSending); }
966
+ /** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
967
+ onFile(cb) { this.#events.on("file-offer", cb); }
968
+ /** Transfer progress: { friendId, fileId, received, total, sending? }. */
969
+ onFileProgress(cb) { this.#events.on("file-progress", cb); }
970
+ /** Transfer complete: { friendId, fileId, name, size, data?, sending? } (data present on the receiver). */
971
+ onFileComplete(cb) { this.#events.on("file-complete", cb); }
972
+ /** Transfer cancelled by the peer: { friendId, fileId, sending }. */
973
+ onFileCancel(cb) { this.#events.on("file-cancel", cb); }
948
974
  onFriendConnection(cb) {
949
975
  this.#events.on("friendConnection", cb);
950
976
  }
@@ -1511,6 +1537,16 @@ export class Peer {
1511
1537
  if (!opened) {
1512
1538
  continue;
1513
1539
  }
1540
+ // De-duplicate. The dual-send path delivers each lossless packet over
1541
+ // BOTH the UDP and relay transports with the SAME packet number, so
1542
+ // without this guard every chat message (and every file-data chunk) is
1543
+ // dispatched — and stored / shown / appended — twice. Drop anything at or
1544
+ // below the receive low-water mark, like toxcore net_crypto drops packets
1545
+ // below buffer_start. Must run BEFORE the nonce advance below so a
1546
+ // duplicate doesn't desync the receive nonce.
1547
+ if (opened.packetNumber < (state.receiveBufferStart ?? 0)) {
1548
+ continue;
1549
+ }
1514
1550
  state.peerBaseNonce[state.peerBaseNonce.length - 2] = packet[1];
1515
1551
  state.peerBaseNonce[state.peerBaseNonce.length - 1] = packet[2];
1516
1552
  incrementNonce(state.peerBaseNonce);
@@ -1539,6 +1575,9 @@ export class Peer {
1539
1575
  state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
1540
1576
  const kind = opened.payload[0];
1541
1577
  const inner = opened.payload.slice(1);
1578
+ // Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
1579
+ if (this.#fileTransfer.handlePacket(friendId, kind, inner))
1580
+ return;
1542
1581
  if (kind === PACKET_ID_ONLINE) {
1543
1582
  this.#setFriendOnline(friendId, remote.address, remote.port);
1544
1583
  this.#debugLog(`crypto online packet received from ${friendId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
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",