@decentnetwork/peer 0.1.81 → 0.1.83
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 +24 -1
- package/dist/compat/packet.js +48 -0
- package/dist/peer.d.ts +18 -1
- package/dist/peer.js +170 -6
- package/dist/types/peer.d.ts +16 -0
- package/package.json +1 -1
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) {
|
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**
|
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, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket } from "./compat/packet.js";
|
|
8
|
+
import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, PACKET_TYPE_BULKMSG, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket } 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, 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";
|
|
@@ -306,6 +306,11 @@ export class Peer {
|
|
|
306
306
|
// #initiateSession every few seconds for every friend; without this
|
|
307
307
|
// dedupe, lower-pubkey side floods the log with "deferring to peer".
|
|
308
308
|
#initiateSkipLogged = new Set();
|
|
309
|
+
// In-flight Carrier BULKMSG reassemblies, keyed `${friendId}:${tid}`.
|
|
310
|
+
// Fragments are appended in arrival order (the lossless stream keeps
|
|
311
|
+
// them ordered) and the message completes when totalsz bytes arrived.
|
|
312
|
+
// Mirrors the C SDK's bulkmsgs hashtable with its 60s assembly timeout.
|
|
313
|
+
#bulkAssembly = new Map();
|
|
309
314
|
// When we FIRST deferred initiation to a higher-pubkey peer (per friend).
|
|
310
315
|
// If the peer hasn't established within the grace window we break the
|
|
311
316
|
// defer and initiate ourselves (see #initiateSession). Cleared whenever a
|
|
@@ -955,6 +960,30 @@ export class Peer {
|
|
|
955
960
|
}
|
|
956
961
|
}
|
|
957
962
|
const packet = encodeFriendMessagePacket(text);
|
|
963
|
+
// Carrier C peers accept at most 1024 raw bytes per friendmsg; larger
|
|
964
|
+
// messages MUST be split into BULKMSG fragments (shared tid, totalsz on
|
|
965
|
+
// the first fragment) or a native receiver drops them. The express
|
|
966
|
+
// fallback below still posts the single big MESSAGE packet — that's
|
|
967
|
+
// what the C SDK does offline too.
|
|
968
|
+
const rawText = new TextEncoder().encode(text);
|
|
969
|
+
let livePackets;
|
|
970
|
+
if (rawText.length > CARRIER_MAX_APP_MESSAGE_LEN) {
|
|
971
|
+
if (rawText.length > CARRIER_MAX_APP_BULKMSG_LEN) {
|
|
972
|
+
throw new Error(`message exceeds bulk limit (${rawText.length} > ${CARRIER_MAX_APP_BULKMSG_LEN})`);
|
|
973
|
+
}
|
|
974
|
+
const tid = (BigInt(Date.now()) << 20n) ^ BigInt(Math.floor(Math.random() * 0xfffff));
|
|
975
|
+
livePackets = [];
|
|
976
|
+
for (let off = 0; off < rawText.length; off += CARRIER_MAX_APP_MESSAGE_LEN) {
|
|
977
|
+
livePackets.push(encodeBulkMsgPacket({
|
|
978
|
+
totalsz: off === 0 ? rawText.length : 0,
|
|
979
|
+
tid,
|
|
980
|
+
data: rawText.subarray(off, Math.min(off + CARRIER_MAX_APP_MESSAGE_LEN, rawText.length))
|
|
981
|
+
}));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
livePackets = [packet];
|
|
986
|
+
}
|
|
958
987
|
// Try the live in-session path first when any transport is up
|
|
959
988
|
// (UDP via session.remote, or TCP relay via session.hasTcpRoute).
|
|
960
989
|
// #sendMessengerPacket internally fans out to both transports and
|
|
@@ -980,7 +1009,9 @@ export class Peer {
|
|
|
980
1009
|
&& provenInbound;
|
|
981
1010
|
if (liveSession) {
|
|
982
1011
|
try {
|
|
983
|
-
|
|
1012
|
+
for (const p of livePackets) {
|
|
1013
|
+
await this.#sendMessengerPacket(pubkey, PACKET_ID_MESSAGE, p);
|
|
1014
|
+
}
|
|
984
1015
|
return;
|
|
985
1016
|
}
|
|
986
1017
|
catch (error) {
|
|
@@ -1060,6 +1091,40 @@ export class Peer {
|
|
|
1060
1091
|
onText(cb) {
|
|
1061
1092
|
this.#events.on("text", cb);
|
|
1062
1093
|
}
|
|
1094
|
+
/** Files received inline over (bulk)messages — the iOS/C Carrier apps'
|
|
1095
|
+
* native way of sending images/audio online (FileModel JSON envelope). */
|
|
1096
|
+
onInlineFile(cb) {
|
|
1097
|
+
this.#events.on("inlineFile", cb);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Send a file INLINE over the message channel the way iOS/C Carrier apps
|
|
1101
|
+
* do: a FileModel JSON envelope ({data: base64, fileExtension, fileName,
|
|
1102
|
+
* type}) carried as a (bulk)message. This is the ONLY file path a native
|
|
1103
|
+
* Beagle client can receive online — the toxcore file transfer (80-82)
|
|
1104
|
+
* used by sendFile() is invisible to the Carrier C SDK. Best for images
|
|
1105
|
+
* and small files; the hard protocol cap is ~5MB (base64-inflated), keep
|
|
1106
|
+
* real use well below it.
|
|
1107
|
+
*/
|
|
1108
|
+
async sendInlineFile(pubkey, opts) {
|
|
1109
|
+
const dot = opts.name.lastIndexOf(".");
|
|
1110
|
+
const ext = dot > 0 ? opts.name.slice(dot).toLowerCase() : "";
|
|
1111
|
+
const base = dot > 0 ? opts.name.slice(0, dot) : opts.name;
|
|
1112
|
+
const type = opts.fileType ?? ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".heic"].includes(ext) ? "image" :
|
|
1113
|
+
[".m4a", ".mp3", ".aac", ".wav", ".ogg"].includes(ext) ? "audio" :
|
|
1114
|
+
[".txt", ".md", ".log"].includes(ext) ? "text" : "unknown");
|
|
1115
|
+
// Key order matters for byte-identical envelopes with iOS (JSONEncoder
|
|
1116
|
+
// .sortedKeys): data, fileExtension, fileName, type — alphabetical.
|
|
1117
|
+
const envelope = JSON.stringify({
|
|
1118
|
+
data: Buffer.from(opts.data).toString("base64"),
|
|
1119
|
+
fileExtension: ext,
|
|
1120
|
+
fileName: base,
|
|
1121
|
+
type
|
|
1122
|
+
});
|
|
1123
|
+
if (envelope.length > CARRIER_MAX_APP_BULKMSG_LEN) {
|
|
1124
|
+
throw new Error(`inline file too large for the message channel (${envelope.length} bytes encoded, max ${CARRIER_MAX_APP_BULKMSG_LEN})`);
|
|
1125
|
+
}
|
|
1126
|
+
await this.sendText(pubkey, envelope);
|
|
1127
|
+
}
|
|
1063
1128
|
/**
|
|
1064
1129
|
* Send an application-defined custom packet to a friend. `id` selects the
|
|
1065
1130
|
* channel and its delivery class by toxcore range: **160–191 lossless**
|
|
@@ -1994,14 +2059,43 @@ export class Peer {
|
|
|
1994
2059
|
return;
|
|
1995
2060
|
}
|
|
1996
2061
|
if (kind === PACKET_ID_MESSAGE || kind === PACKET_ID_ACTION) {
|
|
1997
|
-
|
|
1998
|
-
//
|
|
1999
|
-
|
|
2062
|
+
this.#setFriendOnline(friendId, remote.address, remote.port);
|
|
2063
|
+
// Decode the Carrier FlatBuffers wrapper when present. A BULKMSG
|
|
2064
|
+
// fragment (Carrier C splits any message >1024 bytes this way —
|
|
2065
|
+
// e.g. iOS Beagle images, which are FileModel JSON envelopes)
|
|
2066
|
+
// goes to the reassembler and only emits once complete; before
|
|
2067
|
+
// this, every fragment surfaced as a separate garbage "text".
|
|
2068
|
+
let text;
|
|
2069
|
+
let carrier;
|
|
2070
|
+
try {
|
|
2071
|
+
carrier = decodeCarrierPacket(inner);
|
|
2072
|
+
}
|
|
2073
|
+
catch { /* raw utf-8 peer */ }
|
|
2074
|
+
if (carrier?.type === PACKET_TYPE_BULKMSG) {
|
|
2075
|
+
const complete = this.#assembleBulkMsg(friendId, carrier);
|
|
2076
|
+
if (!complete)
|
|
2077
|
+
return; // more fragments pending
|
|
2078
|
+
text = decodeUtf8Best(complete);
|
|
2079
|
+
this.#debugLog(`bulkmsg complete from ${friendId} (${complete.length} bytes)`);
|
|
2080
|
+
}
|
|
2081
|
+
else if (carrier?.type === PACKET_TYPE_MESSAGE) {
|
|
2082
|
+
text = new TextDecoder().decode(carrier.data);
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
text = decodeUtf8Best(inner);
|
|
2086
|
+
}
|
|
2087
|
+
// Carrier C peers terminate strings with a NUL — strip it so chat
|
|
2088
|
+
// text doesn't carry an invisible trailing byte.
|
|
2089
|
+
text = text?.replace(/\0+$/u, "");
|
|
2000
2090
|
if (!text) {
|
|
2001
2091
|
this.#debugLog(`crypto message packet decode failed from ${friendId}`);
|
|
2002
2092
|
return;
|
|
2003
2093
|
}
|
|
2004
|
-
|
|
2094
|
+
// iOS Beagle sends files inline as a FileModel JSON envelope
|
|
2095
|
+
// ({data: base64, fileExtension, fileName, type}) — surface those
|
|
2096
|
+
// as files, not as a wall of base64 text.
|
|
2097
|
+
if (this.#tryEmitInlineFile(friendId, text, "online"))
|
|
2098
|
+
return;
|
|
2005
2099
|
this.#events.emit("text", {
|
|
2006
2100
|
pubkey: friendId,
|
|
2007
2101
|
text,
|
|
@@ -2370,6 +2464,10 @@ export class Peer {
|
|
|
2370
2464
|
// Malformed pubkey, fall through.
|
|
2371
2465
|
}
|
|
2372
2466
|
}
|
|
2467
|
+
// Offline images/audio arrive as the same FileModel JSON envelope
|
|
2468
|
+
// (iOS posts the whole thing as one express MESSAGE packet).
|
|
2469
|
+
if (this.#tryEmitInlineFile(fromUserId, text, "offline"))
|
|
2470
|
+
return;
|
|
2373
2471
|
this.#events.emit("text", { pubkey: fromUserId, text, via: "offline" });
|
|
2374
2472
|
}
|
|
2375
2473
|
catch {
|
|
@@ -3113,6 +3211,72 @@ export class Peer {
|
|
|
3113
3211
|
});
|
|
3114
3212
|
}
|
|
3115
3213
|
}
|
|
3214
|
+
/** Append a Carrier BULKMSG fragment; returns the full reassembled
|
|
3215
|
+
* message when the last fragment lands, undefined while pending.
|
|
3216
|
+
* Mirrors the C SDK's handle_friend_bulkmsg: totalsz on the first
|
|
3217
|
+
* fragment sizes the buffer; fragments append in arrival order. */
|
|
3218
|
+
#assembleBulkMsg(friendId, frag) {
|
|
3219
|
+
const now = Date.now();
|
|
3220
|
+
// Lazy expiry sweep (C uses a 60s assembly timeout).
|
|
3221
|
+
for (const [key, entry] of this.#bulkAssembly) {
|
|
3222
|
+
if (entry.expireAtMs < now)
|
|
3223
|
+
this.#bulkAssembly.delete(key);
|
|
3224
|
+
}
|
|
3225
|
+
const key = `${friendId}:${frag.tid.toString()}`;
|
|
3226
|
+
let entry = this.#bulkAssembly.get(key);
|
|
3227
|
+
if (!entry) {
|
|
3228
|
+
if (!frag.totalsz || frag.totalsz > CARRIER_MAX_APP_BULKMSG_LEN) {
|
|
3229
|
+
this.#debugLog(`bulkmsg from ${friendId} with invalid/missing totalsz ${frag.totalsz} — dropped`);
|
|
3230
|
+
return undefined;
|
|
3231
|
+
}
|
|
3232
|
+
entry = { total: frag.totalsz, chunks: [], got: 0, expireAtMs: now + 60_000 };
|
|
3233
|
+
this.#bulkAssembly.set(key, entry);
|
|
3234
|
+
}
|
|
3235
|
+
if (frag.data.length === 0 || entry.got + frag.data.length > entry.total) {
|
|
3236
|
+
this.#debugLog(`bulkmsg fragment from ${friendId} overflows totalsz — dropped`);
|
|
3237
|
+
this.#bulkAssembly.delete(key);
|
|
3238
|
+
return undefined;
|
|
3239
|
+
}
|
|
3240
|
+
entry.chunks.push(frag.data);
|
|
3241
|
+
entry.got += frag.data.length;
|
|
3242
|
+
if (entry.got < entry.total)
|
|
3243
|
+
return undefined;
|
|
3244
|
+
this.#bulkAssembly.delete(key);
|
|
3245
|
+
return concatBytes(entry.chunks);
|
|
3246
|
+
}
|
|
3247
|
+
/** iOS Beagle sends files inline as a JSON envelope over (bulk)messages:
|
|
3248
|
+
* {"data":"<base64>","fileExtension":".jpg","fileName":"…","type":"image"}.
|
|
3249
|
+
* Detect it, decode, and emit an inlineFile event instead of dumping
|
|
3250
|
+
* base64 into the chat. Returns true when handled. */
|
|
3251
|
+
#tryEmitInlineFile(friendId, text, via) {
|
|
3252
|
+
// The Carrier C SDK sends messages with a trailing C-string NUL, which
|
|
3253
|
+
// JSON.parse rejects. iOS's own decoder strips NULs before parsing
|
|
3254
|
+
// (CodableUtil.decode: replacingOccurrences(of: "\0")) — mirror that.
|
|
3255
|
+
const trimmed = text.replaceAll("\0", "").trim();
|
|
3256
|
+
if (!trimmed.startsWith("{") || !trimmed.includes("\"fileName\"") || !trimmed.includes("\"data\"")) {
|
|
3257
|
+
return false;
|
|
3258
|
+
}
|
|
3259
|
+
try {
|
|
3260
|
+
const parsed = JSON.parse(trimmed);
|
|
3261
|
+
if (typeof parsed.data !== "string" || typeof parsed.fileName !== "string")
|
|
3262
|
+
return false;
|
|
3263
|
+
const data = Uint8Array.from(Buffer.from(parsed.data, "base64"));
|
|
3264
|
+
const ext = parsed.fileExtension ?? "";
|
|
3265
|
+
const name = parsed.fileName.endsWith(ext) ? parsed.fileName : parsed.fileName + ext;
|
|
3266
|
+
this.#events.emit("inlineFile", {
|
|
3267
|
+
pubkey: friendId,
|
|
3268
|
+
name,
|
|
3269
|
+
fileType: parsed.type,
|
|
3270
|
+
data,
|
|
3271
|
+
via
|
|
3272
|
+
});
|
|
3273
|
+
this.#debugLog(`inline file from ${friendId}: "${name}" (${data.length} bytes, ${via})`);
|
|
3274
|
+
return true;
|
|
3275
|
+
}
|
|
3276
|
+
catch {
|
|
3277
|
+
return false;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3116
3280
|
/** Toxcore handle_request_packet: parse the peer's PACKET_ID_REQUEST and
|
|
3117
3281
|
* (a) implicitly ACK every walked packet number that isn't requested,
|
|
3118
3282
|
* (b) retransmit the requested ones from the send buffer. */
|
package/dist/types/peer.d.ts
CHANGED
|
@@ -114,6 +114,22 @@ export type CustomPacketEvent = {
|
|
|
114
114
|
id: number;
|
|
115
115
|
data: Uint8Array;
|
|
116
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* A file received INLINE over the message channel — the way iOS/C Carrier
|
|
119
|
+
* apps send images/audio online: a FileModel JSON envelope
|
|
120
|
+
* ({data: base64, fileExtension, fileName, type}) carried as a
|
|
121
|
+
* (bulk)message. Distinct from the toxcore file-transfer path (sendFile),
|
|
122
|
+
* which native Carrier clients cannot see.
|
|
123
|
+
*/
|
|
124
|
+
export type InlineFileEvent = {
|
|
125
|
+
pubkey: string;
|
|
126
|
+
/** File name including extension. */
|
|
127
|
+
name: string;
|
|
128
|
+
/** The sender's declared type ("image" | "audio" | "text" | …), if any. */
|
|
129
|
+
fileType?: string;
|
|
130
|
+
data: Uint8Array;
|
|
131
|
+
via: "online" | "offline";
|
|
132
|
+
};
|
|
117
133
|
export type LookupResult = {
|
|
118
134
|
pubkey: string;
|
|
119
135
|
status: "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.83",
|
|
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",
|