@decentnetwork/peer 0.1.41 → 0.1.43

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[];
@@ -147,4 +192,34 @@ export declare class Peer {
147
192
  cookieRequestSentMs: number | null;
148
193
  } | null;
149
194
  waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>;
195
+ /**
196
+ * After a friend sends us PACKET_ID_ONLINE, push our nickname + status
197
+ * message so their UI replaces "unknown" with our actual display name,
198
+ * and optionally fire off a configured greeting message. Idempotent —
199
+ * tracked per friend so we don't spam every keepalive cycle.
200
+ *
201
+ * Sends are sequenced with small delays so the receiving peer's UI layer
202
+ * doesn't process three messenger packets in the same render frame —
203
+ * iOS Beagle 1.8.6 has a SwiftUI use-after-free in
204
+ * `_UIHostingView.beginTransaction()` when nickname / status / message
205
+ * arrive in rapid succession at session establishment, observed as
206
+ * EXC_BAD_ACCESS in the iOS app's main thread.
207
+ */
208
+ /**
209
+ * Update our own profile (display name / status-message description) at
210
+ * runtime and re-push it to friends. `name` maps to the toxcore nickname
211
+ * (PACKET_ID_NICKNAME) and the Carrier USERINFO `name`; `description` maps to
212
+ * the USERINFO descr / status message. Re-arms the per-friend "profile sent"
213
+ * flag so every friend receives the update, and proactively pushes to anyone
214
+ * currently online. The greeting is NOT re-sent (its flag is left intact).
215
+ */
216
+ setUserInfo(info: {
217
+ name?: string;
218
+ description?: string;
219
+ }): void;
220
+ /** Our current display name + status-message description. */
221
+ userInfo(): {
222
+ name: string;
223
+ description: string;
224
+ };
150
225
  }
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
  }
@@ -1539,6 +1565,9 @@ export class Peer {
1539
1565
  state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
1540
1566
  const kind = opened.payload[0];
1541
1567
  const inner = opened.payload.slice(1);
1568
+ // Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
1569
+ if (this.#fileTransfer.handlePacket(friendId, kind, inner))
1570
+ return;
1542
1571
  if (kind === PACKET_ID_ONLINE) {
1543
1572
  this.#setFriendOnline(friendId, remote.address, remote.port);
1544
1573
  this.#debugLog(`crypto online packet received from ${friendId}`);
@@ -2832,6 +2861,34 @@ export class Peer {
2832
2861
  * arrive in rapid succession at session establishment, observed as
2833
2862
  * EXC_BAD_ACCESS in the iOS app's main thread.
2834
2863
  */
2864
+ /**
2865
+ * Update our own profile (display name / status-message description) at
2866
+ * runtime and re-push it to friends. `name` maps to the toxcore nickname
2867
+ * (PACKET_ID_NICKNAME) and the Carrier USERINFO `name`; `description` maps to
2868
+ * the USERINFO descr / status message. Re-arms the per-friend "profile sent"
2869
+ * flag so every friend receives the update, and proactively pushes to anyone
2870
+ * currently online. The greeting is NOT re-sent (its flag is left intact).
2871
+ */
2872
+ setUserInfo(info) {
2873
+ const opts = this.#opts;
2874
+ if (info.name !== undefined)
2875
+ opts.nickname = info.name;
2876
+ if (info.description !== undefined)
2877
+ opts.statusMessage = info.description;
2878
+ this.#profileSentTo.clear();
2879
+ for (const [friendId, s] of this.#friendSessions.entries()) {
2880
+ if (s.established)
2881
+ this.#sendProfileAndGreeting(friendId);
2882
+ }
2883
+ }
2884
+ /** Our current display name + status-message description. */
2885
+ userInfo() {
2886
+ const opts = this.#opts;
2887
+ return {
2888
+ name: opts.nickname ?? PEER_NICKNAME,
2889
+ description: opts.statusMessage ?? PEER_STATUS_MESSAGE,
2890
+ };
2891
+ }
2835
2892
  #sendProfileAndGreeting(friendId) {
2836
2893
  if (!this.#profileSentTo.has(friendId)) {
2837
2894
  this.#profileSentTo.add(friendId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
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",