@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.
- package/dist/compat/filetransfer.d.ts +58 -0
- package/dist/compat/filetransfer.js +240 -0
- package/dist/peer.d.ts +45 -0
- package/dist/peer.js +39 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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",
|