@decentnetwork/peer 0.1.83 → 0.1.85
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 +69 -1
- package/dist/compat/packet.js +112 -0
- package/dist/compat/tcp-relay-pool.d.ts +11 -0
- package/dist/compat/tcp-relay-pool.js +22 -0
- package/dist/compat/tcp-relay.d.ts +13 -0
- package/dist/compat/tcp-relay.js +23 -0
- package/dist/compat/tox-onion.d.ts +27 -0
- package/dist/compat/tox-onion.js +38 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/peer.d.ts +29 -1
- package/dist/peer.js +191 -2
- package/dist/types/peer.d.ts +29 -0
- package/package.json +3 -1
package/dist/compat/packet.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
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_INVITE_REQUEST = 34;
|
|
5
|
+
export declare const PACKET_TYPE_INVITE_RESPONSE = 35;
|
|
4
6
|
export declare const PACKET_TYPE_BULKMSG = 36;
|
|
5
7
|
export declare const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
|
|
6
8
|
export declare const CARRIER_MAX_APP_BULKMSG_LEN: number;
|
|
9
|
+
export declare const INVITE_DATA_UNIT = 1280;
|
|
10
|
+
export declare const CARRIER_MAX_INVITE_DATA_LEN = 8192;
|
|
11
|
+
export declare const CARRIER_MAX_BUNDLE_LEN = 511;
|
|
12
|
+
export declare const CARRIER_MAX_EXTENSION_NAME_LEN = 31;
|
|
13
|
+
export declare const CARRIER_EXTENSION_NAME = "carrier";
|
|
7
14
|
export type UserInfoPacket = {
|
|
8
15
|
type: typeof PACKET_TYPE_USERINFO;
|
|
9
16
|
avatar: boolean;
|
|
@@ -34,7 +41,39 @@ export type BulkMsgPacket = {
|
|
|
34
41
|
tid: bigint;
|
|
35
42
|
data: Uint8Array;
|
|
36
43
|
};
|
|
37
|
-
|
|
44
|
+
/**
|
|
45
|
+
* One `invitereq` fragment (PACKET_TYPE_INVITE_REQUEST = 34). Field layout per
|
|
46
|
+
* carrier/packet.fbs: 0=ext string, 1=tid long, 2=bundle string, 3=totalsz
|
|
47
|
+
* uint, 4=data [uint8]. Mirrors carrier_invite_friend's per-fragment packet.
|
|
48
|
+
*/
|
|
49
|
+
export type InviteReqPacket = {
|
|
50
|
+
type: typeof PACKET_TYPE_INVITE_REQUEST;
|
|
51
|
+
ext?: string;
|
|
52
|
+
tid: bigint;
|
|
53
|
+
bundle?: string;
|
|
54
|
+
/** Full data length — set on the FIRST fragment only, 0 on the rest. */
|
|
55
|
+
totalsz: number;
|
|
56
|
+
data: Uint8Array;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* One `invitersp` fragment (PACKET_TYPE_INVITE_RESPONSE = 35). Field layout:
|
|
60
|
+
* 0=ext, 1=tid, 2=bundle, 3=totalsz, 4=status int, 5=reason string, 6=data.
|
|
61
|
+
* A non-zero status means "refused" (reason set, no data); status 0 means
|
|
62
|
+
* "confirmed" and carries data. The iOS WebRTC receiver never sends these
|
|
63
|
+
* (each RtcSignal is a one-way invitereq), but we decode them for completeness
|
|
64
|
+
* and can emit them if a peer does reply.
|
|
65
|
+
*/
|
|
66
|
+
export type InviteRspPacket = {
|
|
67
|
+
type: typeof PACKET_TYPE_INVITE_RESPONSE;
|
|
68
|
+
ext?: string;
|
|
69
|
+
tid: bigint;
|
|
70
|
+
bundle?: string;
|
|
71
|
+
totalsz: number;
|
|
72
|
+
status: number;
|
|
73
|
+
reason?: string;
|
|
74
|
+
data: Uint8Array;
|
|
75
|
+
};
|
|
76
|
+
export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessagePacket | InviteReqPacket | InviteRspPacket | BulkMsgPacket;
|
|
38
77
|
/**
|
|
39
78
|
* Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
|
|
40
79
|
* this every time the local user's name / status message / profile fields
|
|
@@ -75,4 +114,33 @@ export declare function encodeBulkMsgPacket(opts: {
|
|
|
75
114
|
tid: bigint;
|
|
76
115
|
data: Uint8Array;
|
|
77
116
|
}): Uint8Array;
|
|
117
|
+
/**
|
|
118
|
+
* Encode ONE `invitereq` fragment (PACKET_TYPE_INVITE_REQUEST = 34). Field
|
|
119
|
+
* layout per packet.fbs invitereq: 0=ext, 1=tid, 2=bundle, 3=totalsz, 4=data.
|
|
120
|
+
* Callers fragment the payload (INVITE_DATA_UNIT) and share one tid; only the
|
|
121
|
+
* first fragment sets totalsz (and the optional bundle). Mirrors the do/while
|
|
122
|
+
* in carrier_invite_friend.
|
|
123
|
+
*/
|
|
124
|
+
export declare function encodeInviteReqPacket(opts: {
|
|
125
|
+
ext?: string;
|
|
126
|
+
tid: bigint;
|
|
127
|
+
bundle?: string;
|
|
128
|
+
totalsz: number;
|
|
129
|
+
data: Uint8Array;
|
|
130
|
+
}): Uint8Array;
|
|
131
|
+
/**
|
|
132
|
+
* Encode ONE `invitersp` fragment (PACKET_TYPE_INVITE_RESPONSE = 35). Field
|
|
133
|
+
* layout: 0=ext, 1=tid, 2=bundle, 3=totalsz, 4=status, 5=reason, 6=data.
|
|
134
|
+
* status !== 0 → refused (reason set, no data); status === 0 → confirmed
|
|
135
|
+
* (carries data). Mirrors carrier_reply_friend_invite.
|
|
136
|
+
*/
|
|
137
|
+
export declare function encodeInviteRspPacket(opts: {
|
|
138
|
+
ext?: string;
|
|
139
|
+
tid: bigint;
|
|
140
|
+
bundle?: string;
|
|
141
|
+
totalsz: number;
|
|
142
|
+
status: number;
|
|
143
|
+
reason?: string;
|
|
144
|
+
data?: Uint8Array;
|
|
145
|
+
}): Uint8Array;
|
|
78
146
|
export declare function decodeCarrierPacket(bytes: Uint8Array): CarrierPacket;
|
package/dist/compat/packet.js
CHANGED
|
@@ -2,6 +2,8 @@ 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_INVITE_REQUEST = 34;
|
|
6
|
+
export const PACKET_TYPE_INVITE_RESPONSE = 35;
|
|
5
7
|
export const PACKET_TYPE_BULKMSG = 36;
|
|
6
8
|
// Carrier C SDK message-size constants (carrier.h). A single friendmsg may
|
|
7
9
|
// carry at most 1024 bytes; anything larger is split into BULKMSG fragments
|
|
@@ -9,12 +11,29 @@ export const PACKET_TYPE_BULKMSG = 36;
|
|
|
9
11
|
// only), reassembled in arrival order by the receiver.
|
|
10
12
|
export const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
|
|
11
13
|
export const CARRIER_MAX_APP_BULKMSG_LEN = 5 * 1024 * 1024;
|
|
14
|
+
// Carrier friend-invite constants (carrier.c / carrier.h). An invite carries
|
|
15
|
+
// up to 8192 bytes of application data, fragmented into INVITE_DATA_UNIT-byte
|
|
16
|
+
// chunks sharing one tid — the first fragment carries totalsz (and the
|
|
17
|
+
// optional bundle), the rest carry totalsz=0. This is exactly the transport
|
|
18
|
+
// the iOS/Android WebRTC SDK uses for call signaling (RtcSignal JSON), sent
|
|
19
|
+
// via the "carrier" extension (CarrierExtension.sendInviteFriendRequest →
|
|
20
|
+
// carrier_invite_friend(to="<userid>:carrier", bundle=NULL, ...)).
|
|
21
|
+
export const INVITE_DATA_UNIT = 1280;
|
|
22
|
+
export const CARRIER_MAX_INVITE_DATA_LEN = 8192;
|
|
23
|
+
export const CARRIER_MAX_BUNDLE_LEN = 511;
|
|
24
|
+
export const CARRIER_MAX_EXTENSION_NAME_LEN = 31;
|
|
25
|
+
// The extension name the native SDK registers/sends under; the `ext` field of
|
|
26
|
+
// every invitereq/invitersp on the wire. WebRTC signaling MUST use this so the
|
|
27
|
+
// remote's registerExtension handler fires.
|
|
28
|
+
export const CARRIER_EXTENSION_NAME = "carrier";
|
|
12
29
|
// Body type indices into FlatBuffers `anybody` union (carrier/packet.fbs).
|
|
13
30
|
// The .fbs declares: userinfo, friendreq, friendmsg, invitereq, invitersp,
|
|
14
31
|
// bulkmsg — flatbuffers maps those to 1, 2, 3, 4, 5, 6 (NONE = 0).
|
|
15
32
|
const ANYBODY_USERINFO = 1;
|
|
16
33
|
const ANYBODY_FRIENDREQ = 2;
|
|
17
34
|
const ANYBODY_FRIENDMSG = 3;
|
|
35
|
+
const ANYBODY_INVITEREQ = 4;
|
|
36
|
+
const ANYBODY_INVITERSP = 5;
|
|
18
37
|
const ANYBODY_BULKMSG = 6;
|
|
19
38
|
/**
|
|
20
39
|
* Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
|
|
@@ -100,6 +119,73 @@ export function encodeBulkMsgPacket(opts) {
|
|
|
100
119
|
const bulk = builder.endObject();
|
|
101
120
|
return finishCarrierPacket(builder, PACKET_TYPE_BULKMSG, ANYBODY_BULKMSG, bulk);
|
|
102
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Encode ONE `invitereq` fragment (PACKET_TYPE_INVITE_REQUEST = 34). Field
|
|
124
|
+
* layout per packet.fbs invitereq: 0=ext, 1=tid, 2=bundle, 3=totalsz, 4=data.
|
|
125
|
+
* Callers fragment the payload (INVITE_DATA_UNIT) and share one tid; only the
|
|
126
|
+
* first fragment sets totalsz (and the optional bundle). Mirrors the do/while
|
|
127
|
+
* in carrier_invite_friend.
|
|
128
|
+
*/
|
|
129
|
+
export function encodeInviteReqPacket(opts) {
|
|
130
|
+
const builder = new Builder(opts.data.length + 128);
|
|
131
|
+
const dataOffset = builder.createByteVector(opts.data);
|
|
132
|
+
const bundleOffset = opts.bundle ? builder.createString(opts.bundle) : 0;
|
|
133
|
+
const extOffset = opts.ext ? builder.createString(opts.ext) : 0;
|
|
134
|
+
builder.startObject(5);
|
|
135
|
+
builder.addFieldOffset(4, dataOffset, 0);
|
|
136
|
+
if (opts.totalsz) {
|
|
137
|
+
builder.addFieldInt32(3, opts.totalsz, 0);
|
|
138
|
+
}
|
|
139
|
+
if (bundleOffset) {
|
|
140
|
+
builder.addFieldOffset(2, bundleOffset, 0);
|
|
141
|
+
}
|
|
142
|
+
if (opts.tid !== 0n) {
|
|
143
|
+
builder.addFieldInt64(1, opts.tid, BigInt(0));
|
|
144
|
+
}
|
|
145
|
+
if (extOffset) {
|
|
146
|
+
builder.addFieldOffset(0, extOffset, 0);
|
|
147
|
+
}
|
|
148
|
+
const ireq = builder.endObject();
|
|
149
|
+
return finishCarrierPacket(builder, PACKET_TYPE_INVITE_REQUEST, ANYBODY_INVITEREQ, ireq);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Encode ONE `invitersp` fragment (PACKET_TYPE_INVITE_RESPONSE = 35). Field
|
|
153
|
+
* layout: 0=ext, 1=tid, 2=bundle, 3=totalsz, 4=status, 5=reason, 6=data.
|
|
154
|
+
* status !== 0 → refused (reason set, no data); status === 0 → confirmed
|
|
155
|
+
* (carries data). Mirrors carrier_reply_friend_invite.
|
|
156
|
+
*/
|
|
157
|
+
export function encodeInviteRspPacket(opts) {
|
|
158
|
+
const payload = opts.data ?? new Uint8Array();
|
|
159
|
+
const builder = new Builder(payload.length + 128);
|
|
160
|
+
const dataOffset = payload.length ? builder.createByteVector(payload) : 0;
|
|
161
|
+
const reasonOffset = opts.reason ? builder.createString(opts.reason) : 0;
|
|
162
|
+
const bundleOffset = opts.bundle ? builder.createString(opts.bundle) : 0;
|
|
163
|
+
const extOffset = opts.ext ? builder.createString(opts.ext) : 0;
|
|
164
|
+
builder.startObject(7);
|
|
165
|
+
if (dataOffset) {
|
|
166
|
+
builder.addFieldOffset(6, dataOffset, 0);
|
|
167
|
+
}
|
|
168
|
+
if (reasonOffset) {
|
|
169
|
+
builder.addFieldOffset(5, reasonOffset, 0);
|
|
170
|
+
}
|
|
171
|
+
if (opts.status) {
|
|
172
|
+
builder.addFieldInt32(4, opts.status, 0);
|
|
173
|
+
}
|
|
174
|
+
if (opts.totalsz) {
|
|
175
|
+
builder.addFieldInt32(3, opts.totalsz, 0);
|
|
176
|
+
}
|
|
177
|
+
if (bundleOffset) {
|
|
178
|
+
builder.addFieldOffset(2, bundleOffset, 0);
|
|
179
|
+
}
|
|
180
|
+
if (opts.tid !== 0n) {
|
|
181
|
+
builder.addFieldInt64(1, opts.tid, BigInt(0));
|
|
182
|
+
}
|
|
183
|
+
if (extOffset) {
|
|
184
|
+
builder.addFieldOffset(0, extOffset, 0);
|
|
185
|
+
}
|
|
186
|
+
const irsp = builder.endObject();
|
|
187
|
+
return finishCarrierPacket(builder, PACKET_TYPE_INVITE_RESPONSE, ANYBODY_INVITERSP, irsp);
|
|
188
|
+
}
|
|
103
189
|
export function decodeCarrierPacket(bytes) {
|
|
104
190
|
const bb = new ByteBuffer(bytes);
|
|
105
191
|
const root = bb.readInt32(bb.position()) + bb.position();
|
|
@@ -146,6 +232,28 @@ export function decodeCarrierPacket(bytes) {
|
|
|
146
232
|
data: readByteVectorField(body, 3) ?? new Uint8Array()
|
|
147
233
|
};
|
|
148
234
|
}
|
|
235
|
+
if (type === PACKET_TYPE_INVITE_REQUEST && bodyType === ANYBODY_INVITEREQ) {
|
|
236
|
+
return {
|
|
237
|
+
type,
|
|
238
|
+
ext: readStringField(body, 0) ?? undefined,
|
|
239
|
+
tid: readInt64Field(body, 1),
|
|
240
|
+
bundle: readStringField(body, 2) ?? undefined,
|
|
241
|
+
totalsz: readUint32Field(body, 3),
|
|
242
|
+
data: readByteVectorField(body, 4) ?? new Uint8Array()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (type === PACKET_TYPE_INVITE_RESPONSE && bodyType === ANYBODY_INVITERSP) {
|
|
246
|
+
return {
|
|
247
|
+
type,
|
|
248
|
+
ext: readStringField(body, 0) ?? undefined,
|
|
249
|
+
tid: readInt64Field(body, 1),
|
|
250
|
+
bundle: readStringField(body, 2) ?? undefined,
|
|
251
|
+
totalsz: readUint32Field(body, 3),
|
|
252
|
+
status: readInt32Field(body, 4),
|
|
253
|
+
reason: readStringField(body, 5) ?? undefined,
|
|
254
|
+
data: readByteVectorField(body, 6) ?? new Uint8Array()
|
|
255
|
+
};
|
|
256
|
+
}
|
|
149
257
|
throw new Error(`unsupported carrier packet type/body: ${type}/${bodyType}`);
|
|
150
258
|
}
|
|
151
259
|
function finishCarrierPacket(builder, type, bodyType, bodyOffset) {
|
|
@@ -169,6 +277,10 @@ function readUint32Field(table, field) {
|
|
|
169
277
|
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
170
278
|
return offset ? table.bb.readUint32(table.bb_pos + offset) : 0;
|
|
171
279
|
}
|
|
280
|
+
function readInt32Field(table, field) {
|
|
281
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
282
|
+
return offset ? table.bb.readInt32(table.bb_pos + offset) : 0;
|
|
283
|
+
}
|
|
172
284
|
function readInt64Field(table, field) {
|
|
173
285
|
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
174
286
|
return offset ? table.bb.readInt64(table.bb_pos + offset) : BigInt(0);
|
|
@@ -17,6 +17,8 @@ export type TcpRelayPoolEvents = {
|
|
|
17
17
|
peerData: (friendPublicKey: Uint8Array, payload: Uint8Array) => void;
|
|
18
18
|
/** Inbound OOB_RECV (used for friend requests via TCP). */
|
|
19
19
|
oob: (senderPublicKey: Uint8Array, payload: Uint8Array) => void;
|
|
20
|
+
/** Inbound onion response routed back through a relay (announce / onion data). */
|
|
21
|
+
onionResponse: (packet: Uint8Array) => void;
|
|
20
22
|
/** Number of currently-connected relays changed (for diagnostics). */
|
|
21
23
|
status: (connected: number, total: number) => void;
|
|
22
24
|
};
|
|
@@ -67,6 +69,15 @@ export declare class TcpRelayPool extends EventEmitter {
|
|
|
67
69
|
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array, droppable?: boolean): number;
|
|
68
70
|
/** Diagnostics. */
|
|
69
71
|
connectedCount(): number;
|
|
72
|
+
/**
|
|
73
|
+
* Send an onion request (createOnionRequest0Tcp packet) through up to `fanout`
|
|
74
|
+
* connected relays. Each relay forwards it to node B over UDP and routes the
|
|
75
|
+
* onion response back over TCP (surfaced via the pool's "onionResponse"
|
|
76
|
+
* event). Returns the number of relays the request was handed to (0 = none
|
|
77
|
+
* connected). Fanning out across a few relays covers the case where one relay
|
|
78
|
+
* can't reach node B.
|
|
79
|
+
*/
|
|
80
|
+
sendOnionRequest(packet: Uint8Array, fanout?: number): number;
|
|
70
81
|
/**
|
|
71
82
|
* Snapshot of currently-connected relays for inclusion in DHT-PK
|
|
72
83
|
* announce extras. Mirrors toxcore's `tcp_copy_connected_relays`.
|
|
@@ -211,6 +211,25 @@ export class TcpRelayPool extends EventEmitter {
|
|
|
211
211
|
connectedCount() {
|
|
212
212
|
return this.#relays.filter((r) => r.client?.state() === "connected").length;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Send an onion request (createOnionRequest0Tcp packet) through up to `fanout`
|
|
216
|
+
* connected relays. Each relay forwards it to node B over UDP and routes the
|
|
217
|
+
* onion response back over TCP (surfaced via the pool's "onionResponse"
|
|
218
|
+
* event). Returns the number of relays the request was handed to (0 = none
|
|
219
|
+
* connected). Fanning out across a few relays covers the case where one relay
|
|
220
|
+
* can't reach node B.
|
|
221
|
+
*/
|
|
222
|
+
sendOnionRequest(packet, fanout = 2) {
|
|
223
|
+
let sent = 0;
|
|
224
|
+
for (const r of this.#relays) {
|
|
225
|
+
if (sent >= fanout)
|
|
226
|
+
break;
|
|
227
|
+
if (r.client?.state() === "connected" && r.client.sendOnionRequest(packet)) {
|
|
228
|
+
sent += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return sent;
|
|
232
|
+
}
|
|
214
233
|
/**
|
|
215
234
|
* Snapshot of currently-connected relays for inclusion in DHT-PK
|
|
216
235
|
* announce extras. Mirrors toxcore's `tcp_copy_connected_relays`.
|
|
@@ -304,6 +323,9 @@ export class TcpRelayPool extends EventEmitter {
|
|
|
304
323
|
client.on("oob", (senderKey, payload) => {
|
|
305
324
|
this.emit("oob", new Uint8Array(senderKey), payload);
|
|
306
325
|
});
|
|
326
|
+
client.on("onionResponse", (packet) => {
|
|
327
|
+
this.emit("onionResponse", new Uint8Array(packet));
|
|
328
|
+
});
|
|
307
329
|
try {
|
|
308
330
|
await client.connect(10_000);
|
|
309
331
|
}
|
|
@@ -54,6 +54,10 @@ export type TcpRelayEvents = {
|
|
|
54
54
|
oob: (senderPublicKey: Uint8Array, payload: Uint8Array) => void;
|
|
55
55
|
/** Pong (response to our ping). */
|
|
56
56
|
pong: (pingId: bigint) => void;
|
|
57
|
+
/** Inbound TCP_PACKET_ONION_RESPONSE — the raw onion packet (announce or
|
|
58
|
+
* onion-data response), routed back through this relay. Treat exactly like a
|
|
59
|
+
* UDP onion datagram. */
|
|
60
|
+
onionResponse: (packet: Uint8Array) => void;
|
|
57
61
|
};
|
|
58
62
|
export declare class TcpRelayClient extends EventEmitter {
|
|
59
63
|
#private;
|
|
@@ -85,6 +89,15 @@ export declare class TcpRelayClient extends EventEmitter {
|
|
|
85
89
|
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array, droppable?: boolean): boolean;
|
|
86
90
|
/** Send a PING; relay echoes it as PONG. ping_id is opaque 8 bytes. */
|
|
87
91
|
sendPing(): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Send an onion request THROUGH this relay (toxcore TCP_client.c::
|
|
94
|
+
* send_onion_request). `packet` is a create_onion_packet_tcp payload
|
|
95
|
+
* (createOnionRequest0Tcp) — the relay acts as onion node A, forwards it to
|
|
96
|
+
* node B over UDP, and routes the onion response back to us over this TCP
|
|
97
|
+
* connection as TCP_PACKET_ONION_RESPONSE (surfaced via the "onionResponse"
|
|
98
|
+
* event). This is the NAT-resilient announce / friend-lookup path.
|
|
99
|
+
*/
|
|
100
|
+
sendOnionRequest(packet: Uint8Array): boolean;
|
|
88
101
|
/** Send DATA payload to the friend at `connectionId`. */
|
|
89
102
|
sendData(connectionId: number, payload: Uint8Array, droppable?: boolean): boolean;
|
|
90
103
|
/** Send OOB_SEND (used for delivering friend requests via TCP relay). */
|
package/dist/compat/tcp-relay.js
CHANGED
|
@@ -379,6 +379,16 @@ export class TcpRelayClient extends EventEmitter {
|
|
|
379
379
|
this.emit("oob", sender, payload);
|
|
380
380
|
return;
|
|
381
381
|
}
|
|
382
|
+
case TCP_PACKET_ONION_RESPONSE: {
|
|
383
|
+
// The relay forwarded an onion response addressed to us (announce
|
|
384
|
+
// response / onion data response). `body` is the raw onion packet —
|
|
385
|
+
// identical to what would have arrived over UDP — so the Peer feeds it
|
|
386
|
+
// through the same NET_PACKET_ONION_* handling.
|
|
387
|
+
if (body.length === 0)
|
|
388
|
+
return;
|
|
389
|
+
this.emit("onionResponse", new Uint8Array(body));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
382
392
|
default: {
|
|
383
393
|
if (type >= 16) {
|
|
384
394
|
// DATA forwarded by relay from a connected peer. Resolve
|
|
@@ -445,6 +455,19 @@ export class TcpRelayClient extends EventEmitter {
|
|
|
445
455
|
const pingId = randomBytes(8);
|
|
446
456
|
return this.#sendFrame(concatBytes([Uint8Array.of(TCP_PACKET_PING), pingId]));
|
|
447
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Send an onion request THROUGH this relay (toxcore TCP_client.c::
|
|
460
|
+
* send_onion_request). `packet` is a create_onion_packet_tcp payload
|
|
461
|
+
* (createOnionRequest0Tcp) — the relay acts as onion node A, forwards it to
|
|
462
|
+
* node B over UDP, and routes the onion response back to us over this TCP
|
|
463
|
+
* connection as TCP_PACKET_ONION_RESPONSE (surfaced via the "onionResponse"
|
|
464
|
+
* event). This is the NAT-resilient announce / friend-lookup path.
|
|
465
|
+
*/
|
|
466
|
+
sendOnionRequest(packet) {
|
|
467
|
+
if (packet.length === 0)
|
|
468
|
+
return false;
|
|
469
|
+
return this.#sendFrame(concatBytes([Uint8Array.of(TCP_PACKET_ONION_REQUEST), packet]));
|
|
470
|
+
}
|
|
448
471
|
/** Send DATA payload to the friend at `connectionId`. */
|
|
449
472
|
sendData(connectionId, payload, droppable = false) {
|
|
450
473
|
if (connectionId < 16 || connectionId > 0xff)
|
|
@@ -49,6 +49,33 @@ export declare function createOnionRequest0(opts: {
|
|
|
49
49
|
nodeDPort: number;
|
|
50
50
|
payloadForNodeD: Uint8Array;
|
|
51
51
|
}): Uint8Array;
|
|
52
|
+
/**
|
|
53
|
+
* Build an onion request to send OVER A TCP RELAY (toxcore
|
|
54
|
+
* onion.c::create_onion_packet_tcp). When the onion path's first hop is a TCP
|
|
55
|
+
* relay we're already connected to, the relay itself acts as node A — so the
|
|
56
|
+
* packet OMITS node A's layer and is sent as the payload of TCP_PACKET_ONION_
|
|
57
|
+
* REQUEST. The relay forwards it to node B over UDP and routes the response
|
|
58
|
+
* back to us over the same TCP connection (TCP_PACKET_ONION_RESPONSE).
|
|
59
|
+
*
|
|
60
|
+
* This is the NAT-resilient discovery path native Beagle uses as primary:
|
|
61
|
+
* self-announce and friend-route lookups survive a NAT that drops the UDP
|
|
62
|
+
* onion return path, because the TCP relay handles the round-trip.
|
|
63
|
+
*
|
|
64
|
+
* Layout (== create_onion_packet_tcp): the UDP builder's inner `bPart` with a
|
|
65
|
+
* bare nonce prepended (no NET_PACKET_ONION_REQUEST_0 type byte, no node-A box):
|
|
66
|
+
* [nonce 24][ipport(B)][tempPubB 32][box_B([ipport(C)][tempPubC 32][box_C([ipport(D)][payload])])]
|
|
67
|
+
*/
|
|
68
|
+
export declare function createOnionRequest0Tcp(opts: {
|
|
69
|
+
nodeBHost: string;
|
|
70
|
+
nodeBPort: number;
|
|
71
|
+
nodeBPublicKey: Uint8Array;
|
|
72
|
+
nodeCHost: string;
|
|
73
|
+
nodeCPort: number;
|
|
74
|
+
nodeCPublicKey: Uint8Array;
|
|
75
|
+
nodeDHost: string;
|
|
76
|
+
nodeDPort: number;
|
|
77
|
+
payloadForNodeD: Uint8Array;
|
|
78
|
+
}): Uint8Array;
|
|
52
79
|
export declare function openOnionDataResponse(packet: Uint8Array, opts: {
|
|
53
80
|
dataSecretKey: Uint8Array;
|
|
54
81
|
}): {
|
package/dist/compat/tox-onion.js
CHANGED
|
@@ -112,6 +112,44 @@ export function createOnionRequest0(opts) {
|
|
|
112
112
|
aEncrypted
|
|
113
113
|
]);
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Build an onion request to send OVER A TCP RELAY (toxcore
|
|
117
|
+
* onion.c::create_onion_packet_tcp). When the onion path's first hop is a TCP
|
|
118
|
+
* relay we're already connected to, the relay itself acts as node A — so the
|
|
119
|
+
* packet OMITS node A's layer and is sent as the payload of TCP_PACKET_ONION_
|
|
120
|
+
* REQUEST. The relay forwards it to node B over UDP and routes the response
|
|
121
|
+
* back to us over the same TCP connection (TCP_PACKET_ONION_RESPONSE).
|
|
122
|
+
*
|
|
123
|
+
* This is the NAT-resilient discovery path native Beagle uses as primary:
|
|
124
|
+
* self-announce and friend-route lookups survive a NAT that drops the UDP
|
|
125
|
+
* onion return path, because the TCP relay handles the round-trip.
|
|
126
|
+
*
|
|
127
|
+
* Layout (== create_onion_packet_tcp): the UDP builder's inner `bPart` with a
|
|
128
|
+
* bare nonce prepended (no NET_PACKET_ONION_REQUEST_0 type byte, no node-A box):
|
|
129
|
+
* [nonce 24][ipport(B)][tempPubB 32][box_B([ipport(C)][tempPubC 32][box_C([ipport(D)][payload])])]
|
|
130
|
+
*/
|
|
131
|
+
export function createOnionRequest0Tcp(opts) {
|
|
132
|
+
ensureLen(opts.nodeBPublicKey, KEY_SIZE, "node B public key");
|
|
133
|
+
ensureLen(opts.nodeCPublicKey, KEY_SIZE, "node C public key");
|
|
134
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
135
|
+
const dPart = concatBytes([packIpPort(opts.nodeDHost, opts.nodeDPort), opts.payloadForNodeD]);
|
|
136
|
+
const key2 = nacl.box.keyPair();
|
|
137
|
+
const cEncrypted = nacl.box(dPart, nonce, opts.nodeCPublicKey, key2.secretKey);
|
|
138
|
+
const cPart = concatBytes([
|
|
139
|
+
packIpPort(opts.nodeCHost, opts.nodeCPort),
|
|
140
|
+
key2.publicKey,
|
|
141
|
+
cEncrypted
|
|
142
|
+
]);
|
|
143
|
+
const key1 = nacl.box.keyPair();
|
|
144
|
+
const bEncrypted = nacl.box(cPart, nonce, opts.nodeBPublicKey, key1.secretKey);
|
|
145
|
+
// No node-A box: the relay is node A. Just [nonce][ipport(B)][tempPubB][box_B].
|
|
146
|
+
return concatBytes([
|
|
147
|
+
nonce,
|
|
148
|
+
packIpPort(opts.nodeBHost, opts.nodeBPort),
|
|
149
|
+
key1.publicKey,
|
|
150
|
+
bEncrypted
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
115
153
|
export function openOnionDataResponse(packet, opts) {
|
|
116
154
|
if (packet.length < 1 + NONCE_SIZE + KEY_SIZE + 16) {
|
|
117
155
|
return undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { Peer } from "./peer.js";
|
|
2
2
|
export { CARRIER_ADDRESS_SIZE, CARRIER_PUBLIC_KEY_SIZE, carrierAddressFromPublicKey, carrierIdFromAddress, carrierIdFromPublicKey, parseCarrierAddress } from "./compat/address.js";
|
|
3
|
-
export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket } from "./compat/packet.js";
|
|
3
|
+
export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, PACKET_TYPE_INVITE_REQUEST, PACKET_TYPE_INVITE_RESPONSE, INVITE_DATA_UNIT, CARRIER_MAX_INVITE_DATA_LEN, CARRIER_EXTENSION_NAME, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeInviteReqPacket, encodeInviteRspPacket } from "./compat/packet.js";
|
|
4
4
|
export { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, CRYPTO_PACKET_NAT_PING, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
5
5
|
export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_REQUEST, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID } from "./compat/tox-onion.js";
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
|
7
7
|
export { base58ToBytes, bytesToBase58 } from "./utils/base58.js";
|
|
8
8
|
export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
|
|
9
|
-
export type { CarrierPacket, FriendMessagePacket, FriendRequestPacket } from "./compat/packet.js";
|
|
9
|
+
export type { CarrierPacket, FriendMessagePacket, FriendRequestPacket, InviteReqPacket, InviteRspPacket } from "./compat/packet.js";
|
|
10
10
|
export type { ToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
11
11
|
export type { CarrierAddressParts } from "./compat/address.js";
|
|
12
|
-
export type { CompatibilityMode, CustomPacketEvent, FriendConnectionEvent, FriendConnectionStatus, FriendInfoEvent, FriendRequest, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
|
12
|
+
export type { CompatibilityMode, CustomPacketEvent, FriendConnectionEvent, FriendConnectionStatus, FriendInfoEvent, FriendRequest, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Peer } from "./peer.js";
|
|
2
2
|
export { CARRIER_ADDRESS_SIZE, CARRIER_PUBLIC_KEY_SIZE, carrierAddressFromPublicKey, carrierIdFromAddress, carrierIdFromPublicKey, parseCarrierAddress } from "./compat/address.js";
|
|
3
|
-
export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket } from "./compat/packet.js";
|
|
3
|
+
export { PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_MESSAGE, PACKET_TYPE_INVITE_REQUEST, PACKET_TYPE_INVITE_RESPONSE, INVITE_DATA_UNIT, CARRIER_MAX_INVITE_DATA_LEN, CARRIER_EXTENSION_NAME, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeInviteReqPacket, encodeInviteRspPacket } from "./compat/packet.js";
|
|
4
4
|
export { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, CRYPTO_PACKET_NAT_PING, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
5
5
|
export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_REQUEST, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID } from "./compat/tox-onion.js";
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
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, InlineFileEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
|
3
|
+
import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
|
|
4
4
|
export declare class Peer {
|
|
5
5
|
#private;
|
|
6
6
|
private constructor();
|
|
@@ -99,6 +99,34 @@ export declare class Peer {
|
|
|
99
99
|
data: Uint8Array;
|
|
100
100
|
fileType?: "image" | "audio" | "text" | "unknown";
|
|
101
101
|
}): Promise<void>;
|
|
102
|
+
/** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST).
|
|
103
|
+
* This is the channel the iOS/Android WebRTC SDK uses for call signaling —
|
|
104
|
+
* each RtcSignal arrives as an "invite" with ext="carrier" and `data` the
|
|
105
|
+
* reassembled JSON. */
|
|
106
|
+
onInvite(cb: (evt: InviteEvent) => void): void;
|
|
107
|
+
/** Fired when a peer replies to one of our invites (PACKET_TYPE_INVITE_RESPONSE).
|
|
108
|
+
* Not used by the one-way WebRTC signaling flow, but surfaced for peers that
|
|
109
|
+
* do reply. */
|
|
110
|
+
onInviteResponse(cb: (evt: InviteResponseEvent) => void): void;
|
|
111
|
+
/**
|
|
112
|
+
* Send a Carrier friend-invite to a peer — the transport the iOS/Android
|
|
113
|
+
* Elastos WebRTC SDK listens on (CarrierExtension.registerExtension). Used
|
|
114
|
+
* for realtime call signaling: pass the RtcSignal JSON as `data` and the
|
|
115
|
+
* remote's registerExtension handler fires with it.
|
|
116
|
+
*
|
|
117
|
+
* Wire-compatible with carrier_invite_friend: sends PACKET_TYPE_INVITE_REQUEST
|
|
118
|
+
* packets over the reliable message channel, `ext` = "carrier" by default,
|
|
119
|
+
* fragmented into INVITE_DATA_UNIT (1280-byte) chunks sharing one tid (first
|
|
120
|
+
* fragment carries totalsz + optional bundle). Signaling is realtime, so this
|
|
121
|
+
* requires a live session and does NOT fall back to offline express — a call
|
|
122
|
+
* signal to an unreachable peer should fail fast, not queue.
|
|
123
|
+
*
|
|
124
|
+
* @returns the tid used (lets callers correlate a later invite-response).
|
|
125
|
+
*/
|
|
126
|
+
sendInvite(pubkey: string, data: Uint8Array | string, opts?: {
|
|
127
|
+
ext?: string | null;
|
|
128
|
+
bundle?: string;
|
|
129
|
+
}): Promise<bigint>;
|
|
102
130
|
/**
|
|
103
131
|
* Send an application-defined custom packet to a friend. `id` selects the
|
|
104
132
|
* channel and its delivery class by toxcore range: **160–191 lossless**
|
package/dist/peer.js
CHANGED
|
@@ -5,8 +5,8 @@ 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, PACKET_TYPE_BULKMSG, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket } from "./compat/packet.js";
|
|
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";
|
|
8
|
+
import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, PACKET_TYPE_BULKMSG, PACKET_TYPE_INVITE_REQUEST, PACKET_TYPE_INVITE_RESPONSE, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, INVITE_DATA_UNIT, CARRIER_MAX_INVITE_DATA_LEN, CARRIER_EXTENSION_NAME, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket, encodeInviteReqPacket } from "./compat/packet.js";
|
|
9
|
+
import { NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID, createOnionAnnounceRequest, createOnionDataPacket, createOnionDataRequest, createOnionRequest0, createOnionRequest0Tcp, 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
12
|
import { FileTransferManager, PACKET_ID_FILE_SENDREQUEST, PACKET_ID_FILE_CONTROL, PACKET_ID_FILE_DATA, PACKET_ID_FILE_FEC } from "./compat/filetransfer.js";
|
|
@@ -311,6 +311,10 @@ export class Peer {
|
|
|
311
311
|
// them ordered) and the message completes when totalsz bytes arrived.
|
|
312
312
|
// Mirrors the C SDK's bulkmsgs hashtable with its 60s assembly timeout.
|
|
313
313
|
#bulkAssembly = new Map();
|
|
314
|
+
// Reassembly buffer for multi-fragment Carrier invites (WebRTC signaling).
|
|
315
|
+
// Keyed by `${friendId}:${tid}`; namespaced separately from #bulkAssembly so
|
|
316
|
+
// a bulkmsg and an invite that happen to share a tid can't collide.
|
|
317
|
+
#inviteAssembly = new Map();
|
|
314
318
|
// When we FIRST deferred initiation to a higher-pubkey peer (per friend).
|
|
315
319
|
// If the peer hasn't established within the grace window we break the
|
|
316
320
|
// defer and initiate ourselves (see #initiateSession). Cleared whenever a
|
|
@@ -507,6 +511,22 @@ export class Peer {
|
|
|
507
511
|
this.#tcpRelays?.requestRoute(senderKey);
|
|
508
512
|
this.#handleTcpDatagram(senderKey, payload);
|
|
509
513
|
});
|
|
514
|
+
// Onion responses routed back through a TCP relay (announce responses,
|
|
515
|
+
// onion data responses). This is the NAT-resilient discovery path: when
|
|
516
|
+
// UDP onion is blocked, the relay does the round-trip. Inject the raw
|
|
517
|
+
// onion packet into the SAME "datagram" stream the UDP transport feeds,
|
|
518
|
+
// so #onDatagram AND the ad-hoc announce/route waiters
|
|
519
|
+
// (#waitForAnnounceResponse) process it identically. Synthetic remote so
|
|
520
|
+
// no real UDP endpoint is adopted from it.
|
|
521
|
+
this.#tcpRelays.on("onionResponse", (packet) => {
|
|
522
|
+
if (packet.length === 0)
|
|
523
|
+
return;
|
|
524
|
+
this.#udp.emit("datagram", {
|
|
525
|
+
data: Buffer.from(packet),
|
|
526
|
+
remote: { address: "tcp-relay", port: 0 },
|
|
527
|
+
viaRelay: true
|
|
528
|
+
});
|
|
529
|
+
});
|
|
510
530
|
// Don't await — pool reconnects on its own and we want start() to
|
|
511
531
|
// return promptly so the demo can call joinNetwork() in parallel.
|
|
512
532
|
void this.#tcpRelays.start().catch((error) => {
|
|
@@ -1125,6 +1145,81 @@ export class Peer {
|
|
|
1125
1145
|
}
|
|
1126
1146
|
await this.sendText(pubkey, envelope);
|
|
1127
1147
|
}
|
|
1148
|
+
/** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST).
|
|
1149
|
+
* This is the channel the iOS/Android WebRTC SDK uses for call signaling —
|
|
1150
|
+
* each RtcSignal arrives as an "invite" with ext="carrier" and `data` the
|
|
1151
|
+
* reassembled JSON. */
|
|
1152
|
+
onInvite(cb) {
|
|
1153
|
+
this.#events.on("invite", cb);
|
|
1154
|
+
}
|
|
1155
|
+
/** Fired when a peer replies to one of our invites (PACKET_TYPE_INVITE_RESPONSE).
|
|
1156
|
+
* Not used by the one-way WebRTC signaling flow, but surfaced for peers that
|
|
1157
|
+
* do reply. */
|
|
1158
|
+
onInviteResponse(cb) {
|
|
1159
|
+
this.#events.on("inviteResponse", cb);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Send a Carrier friend-invite to a peer — the transport the iOS/Android
|
|
1163
|
+
* Elastos WebRTC SDK listens on (CarrierExtension.registerExtension). Used
|
|
1164
|
+
* for realtime call signaling: pass the RtcSignal JSON as `data` and the
|
|
1165
|
+
* remote's registerExtension handler fires with it.
|
|
1166
|
+
*
|
|
1167
|
+
* Wire-compatible with carrier_invite_friend: sends PACKET_TYPE_INVITE_REQUEST
|
|
1168
|
+
* packets over the reliable message channel, `ext` = "carrier" by default,
|
|
1169
|
+
* fragmented into INVITE_DATA_UNIT (1280-byte) chunks sharing one tid (first
|
|
1170
|
+
* fragment carries totalsz + optional bundle). Signaling is realtime, so this
|
|
1171
|
+
* requires a live session and does NOT fall back to offline express — a call
|
|
1172
|
+
* signal to an unreachable peer should fail fast, not queue.
|
|
1173
|
+
*
|
|
1174
|
+
* @returns the tid used (lets callers correlate a later invite-response).
|
|
1175
|
+
*/
|
|
1176
|
+
async sendInvite(pubkey, data, opts = {}) {
|
|
1177
|
+
const friend = this.#friends.get(pubkey);
|
|
1178
|
+
if (!friend) {
|
|
1179
|
+
throw new Error(`Not a friend: ${pubkey}`);
|
|
1180
|
+
}
|
|
1181
|
+
const payload = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1182
|
+
if (payload.length === 0 || payload.length > CARRIER_MAX_INVITE_DATA_LEN) {
|
|
1183
|
+
throw new Error(`invite data must be 1..${CARRIER_MAX_INVITE_DATA_LEN} bytes (got ${payload.length})`);
|
|
1184
|
+
}
|
|
1185
|
+
// `ext: null` sends a plain invite (no extension); default is the "carrier"
|
|
1186
|
+
// extension the native WebRTC SDK registers under.
|
|
1187
|
+
const ext = opts.ext === null ? undefined : (opts.ext ?? CARRIER_EXTENSION_NAME);
|
|
1188
|
+
const bundle = opts.bundle;
|
|
1189
|
+
let session = this.#friendSessions.get(pubkey);
|
|
1190
|
+
if (!session?.established) {
|
|
1191
|
+
await this.#initiateSession(pubkey).catch(() => { });
|
|
1192
|
+
const established = await this.#waitForFriendConnected(pubkey, 5000).catch(() => false);
|
|
1193
|
+
if (established)
|
|
1194
|
+
session = this.#friendSessions.get(pubkey);
|
|
1195
|
+
}
|
|
1196
|
+
if (!session?.established || !session.sessionSharedKey || !session.ourBaseNonce || !(session.remote || session.hasTcpRoute)) {
|
|
1197
|
+
throw new Error(`cannot send invite: no live session to ${pubkey} (peer offline?)`);
|
|
1198
|
+
}
|
|
1199
|
+
// Fragment exactly like carrier_invite_friend: first fragment carries
|
|
1200
|
+
// totalsz and the optional bundle (which counts against the first unit),
|
|
1201
|
+
// the rest carry totalsz=0. bundle is usually absent for WebRTC.
|
|
1202
|
+
const tid = (BigInt(Date.now()) << 20n) ^ BigInt(Math.floor(Math.random() * 0xfffff)) | 1n;
|
|
1203
|
+
const bundleLen = bundle ? new TextEncoder().encode(bundle).length : 0;
|
|
1204
|
+
let off = 0;
|
|
1205
|
+
let first = true;
|
|
1206
|
+
while (off < payload.length) {
|
|
1207
|
+
const unit = first && bundle ? INVITE_DATA_UNIT - bundleLen : INVITE_DATA_UNIT;
|
|
1208
|
+
const end = Math.min(off + unit, payload.length);
|
|
1209
|
+
const p = encodeInviteReqPacket({
|
|
1210
|
+
ext,
|
|
1211
|
+
tid,
|
|
1212
|
+
bundle: first ? bundle : undefined,
|
|
1213
|
+
totalsz: first ? payload.length : 0,
|
|
1214
|
+
data: payload.subarray(off, end)
|
|
1215
|
+
});
|
|
1216
|
+
await this.#sendMessengerPacket(pubkey, PACKET_ID_MESSAGE, p);
|
|
1217
|
+
off = end;
|
|
1218
|
+
first = false;
|
|
1219
|
+
}
|
|
1220
|
+
this.#debugLog(`invite sent to ${pubkey}: ext="${ext ?? ""}" (${payload.length} bytes, tid=${tid})`);
|
|
1221
|
+
return tid;
|
|
1222
|
+
}
|
|
1128
1223
|
/**
|
|
1129
1224
|
* Send an application-defined custom packet to a friend. `id` selects the
|
|
1130
1225
|
* channel and its delivery class by toxcore range: **160–191 lossless**
|
|
@@ -2071,6 +2166,39 @@ export class Peer {
|
|
|
2071
2166
|
carrier = decodeCarrierPacket(inner);
|
|
2072
2167
|
}
|
|
2073
2168
|
catch { /* raw utf-8 peer */ }
|
|
2169
|
+
// Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST/RESPONSE = 34/35)
|
|
2170
|
+
// rides this same PACKET_ID_MESSAGE channel — it's what the iOS/Android
|
|
2171
|
+
// WebRTC SDK uses for call signaling via the "carrier" extension. Large
|
|
2172
|
+
// signals (SDP offers) fragment by tid like bulkmsg; reassemble then
|
|
2173
|
+
// surface as an "invite" event (NOT chat text).
|
|
2174
|
+
if (carrier?.type === PACKET_TYPE_INVITE_REQUEST) {
|
|
2175
|
+
const complete = this.#assembleInvite(friendId, carrier);
|
|
2176
|
+
if (!complete)
|
|
2177
|
+
return; // more fragments pending
|
|
2178
|
+
this.#events.emit("invite", {
|
|
2179
|
+
pubkey: friendId,
|
|
2180
|
+
ext: carrier.ext,
|
|
2181
|
+
bundle: carrier.bundle,
|
|
2182
|
+
data: complete
|
|
2183
|
+
});
|
|
2184
|
+
this.#debugLog(`invite from ${friendId}: ext="${carrier.ext ?? ""}" (${complete.length} bytes)`);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (carrier?.type === PACKET_TYPE_INVITE_RESPONSE) {
|
|
2188
|
+
const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier) : new Uint8Array();
|
|
2189
|
+
if (carrier.status === 0 && !complete)
|
|
2190
|
+
return; // more fragments pending
|
|
2191
|
+
this.#events.emit("inviteResponse", {
|
|
2192
|
+
pubkey: friendId,
|
|
2193
|
+
ext: carrier.ext,
|
|
2194
|
+
bundle: carrier.bundle,
|
|
2195
|
+
status: carrier.status,
|
|
2196
|
+
reason: carrier.reason,
|
|
2197
|
+
data: complete ?? new Uint8Array()
|
|
2198
|
+
});
|
|
2199
|
+
this.#debugLog(`invite-response from ${friendId}: status=${carrier.status} ext="${carrier.ext ?? ""}"`);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2074
2202
|
if (carrier?.type === PACKET_TYPE_BULKMSG) {
|
|
2075
2203
|
const complete = this.#assembleBulkMsg(friendId, carrier);
|
|
2076
2204
|
if (!complete)
|
|
@@ -3244,6 +3372,41 @@ export class Peer {
|
|
|
3244
3372
|
this.#bulkAssembly.delete(key);
|
|
3245
3373
|
return concatBytes(entry.chunks);
|
|
3246
3374
|
}
|
|
3375
|
+
/** Append a Carrier invite fragment (INVITE_REQUEST/RESPONSE); returns the
|
|
3376
|
+
* full reassembled payload when the last fragment lands, undefined while
|
|
3377
|
+
* pending. Mirrors #assembleBulkMsg but caps at CARRIER_MAX_INVITE_DATA_LEN
|
|
3378
|
+
* (8192) and uses a separate buffer. Single-fragment invites (the common
|
|
3379
|
+
* case for small WebRTC signals) complete on the first call. */
|
|
3380
|
+
#assembleInvite(friendId, frag) {
|
|
3381
|
+
const now = Date.now();
|
|
3382
|
+
for (const [key, entry] of this.#inviteAssembly) {
|
|
3383
|
+
if (entry.expireAtMs < now)
|
|
3384
|
+
this.#inviteAssembly.delete(key);
|
|
3385
|
+
}
|
|
3386
|
+
const key = `${friendId}:${frag.tid.toString()}`;
|
|
3387
|
+
let entry = this.#inviteAssembly.get(key);
|
|
3388
|
+
if (!entry) {
|
|
3389
|
+
// First fragment carries totalsz. A lone fragment whose data already
|
|
3390
|
+
// equals totalsz completes immediately below.
|
|
3391
|
+
if (!frag.totalsz || frag.totalsz > CARRIER_MAX_INVITE_DATA_LEN) {
|
|
3392
|
+
this.#debugLog(`invite from ${friendId} with invalid/missing totalsz ${frag.totalsz} — dropped`);
|
|
3393
|
+
return undefined;
|
|
3394
|
+
}
|
|
3395
|
+
entry = { total: frag.totalsz, chunks: [], got: 0, expireAtMs: now + 60_000 };
|
|
3396
|
+
this.#inviteAssembly.set(key, entry);
|
|
3397
|
+
}
|
|
3398
|
+
if (frag.data.length === 0 || entry.got + frag.data.length > entry.total) {
|
|
3399
|
+
this.#debugLog(`invite fragment from ${friendId} overflows totalsz — dropped`);
|
|
3400
|
+
this.#inviteAssembly.delete(key);
|
|
3401
|
+
return undefined;
|
|
3402
|
+
}
|
|
3403
|
+
entry.chunks.push(frag.data);
|
|
3404
|
+
entry.got += frag.data.length;
|
|
3405
|
+
if (entry.got < entry.total)
|
|
3406
|
+
return undefined;
|
|
3407
|
+
this.#inviteAssembly.delete(key);
|
|
3408
|
+
return concatBytes(entry.chunks);
|
|
3409
|
+
}
|
|
3247
3410
|
/** iOS Beagle sends files inline as a JSON envelope over (bulk)messages:
|
|
3248
3411
|
* {"data":"<base64>","fileExtension":".jpg","fileName":"…","type":"image"}.
|
|
3249
3412
|
* Detect it, decode, and emit an inlineFile event instead of dumping
|
|
@@ -5096,6 +5259,32 @@ export class Peer {
|
|
|
5096
5259
|
payloadForNodeD
|
|
5097
5260
|
});
|
|
5098
5261
|
await this.#sendPacket(packet, path.nodeA.node);
|
|
5262
|
+
// NAT-resilient duplicate: ALSO send this onion request over connected TCP
|
|
5263
|
+
// relays (toxcore create_onion_packet_tcp). The relay acts as node A and
|
|
5264
|
+
// forwards to node B over UDP, routing the response back over TCP — so
|
|
5265
|
+
// self-announce AND friend-route discovery survive a NAT that drops the UDP
|
|
5266
|
+
// onion return path (the residential-ISP case where selfAnnounceStoredOn
|
|
5267
|
+
// stays 0 and native peers can never find us). Same sendBack as the UDP
|
|
5268
|
+
// attempt, so whichever transport answers first resolves the waiter.
|
|
5269
|
+
if (this.#tcpRelays && this.#tcpRelays.connectedCount() > 0) {
|
|
5270
|
+
try {
|
|
5271
|
+
const tcpPacket = createOnionRequest0Tcp({
|
|
5272
|
+
nodeBHost: path.nodeB.host,
|
|
5273
|
+
nodeBPort: path.nodeB.port,
|
|
5274
|
+
nodeBPublicKey: path.nodeB.publicKey,
|
|
5275
|
+
nodeCHost: path.nodeC.host,
|
|
5276
|
+
nodeCPort: path.nodeC.port,
|
|
5277
|
+
nodeCPublicKey: path.nodeC.publicKey,
|
|
5278
|
+
nodeDHost: nodeD.host,
|
|
5279
|
+
nodeDPort: nodeD.port,
|
|
5280
|
+
payloadForNodeD
|
|
5281
|
+
});
|
|
5282
|
+
this.#tcpRelays.sendOnionRequest(tcpPacket);
|
|
5283
|
+
}
|
|
5284
|
+
catch {
|
|
5285
|
+
/* malformed path node key — the UDP attempt above still stands */
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5099
5288
|
return path;
|
|
5100
5289
|
}
|
|
5101
5290
|
#selectOnionPath(nodeD, pathOffset = 0) {
|
package/dist/types/peer.d.ts
CHANGED
|
@@ -130,6 +130,35 @@ export type InlineFileEvent = {
|
|
|
130
130
|
data: Uint8Array;
|
|
131
131
|
via: "online" | "offline";
|
|
132
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* A Carrier friend-invite request received from a peer (PACKET_TYPE_INVITE_REQUEST).
|
|
135
|
+
* This is the transport the iOS/Android Elastos WebRTC SDK uses for call
|
|
136
|
+
* signaling: each RtcSignal (offer/answer/candidate/bye JSON) is sent as a
|
|
137
|
+
* one-way invite under the "carrier" extension. `data` is the reassembled
|
|
138
|
+
* application payload (JSON for WebRTC; NUL-terminated by native C senders).
|
|
139
|
+
*/
|
|
140
|
+
export type InviteEvent = {
|
|
141
|
+
pubkey: string;
|
|
142
|
+
/** Extension name — "carrier" for WebRTC/extension invites, undefined for a plain invite. */
|
|
143
|
+
ext?: string;
|
|
144
|
+
/** Optional bundle string set by the sender (usually absent). */
|
|
145
|
+
bundle?: string;
|
|
146
|
+
data: Uint8Array;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* A Carrier friend-invite response (PACKET_TYPE_INVITE_RESPONSE). The iOS
|
|
150
|
+
* WebRTC receiver never sends these (signaling is one-way invitereq both
|
|
151
|
+
* ways), but a peer that calls reply_friend_invite produces one. status 0 =
|
|
152
|
+
* confirmed (carries data); non-zero = refused (reason set, no data).
|
|
153
|
+
*/
|
|
154
|
+
export type InviteResponseEvent = {
|
|
155
|
+
pubkey: string;
|
|
156
|
+
ext?: string;
|
|
157
|
+
bundle?: string;
|
|
158
|
+
status: number;
|
|
159
|
+
reason?: string;
|
|
160
|
+
data: Uint8Array;
|
|
161
|
+
};
|
|
133
162
|
export type LookupResult = {
|
|
134
163
|
pubkey: string;
|
|
135
164
|
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.85",
|
|
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",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"demo:js-to-js": "pnpm run build && node scripts/js-to-js-demo.mjs",
|
|
68
68
|
"demo:tox-dht-crypto": "pnpm run build && node scripts/tox-dht-crypto-demo.mjs",
|
|
69
69
|
"test:compat": "pnpm run build && node scripts/compat-selftest.mjs",
|
|
70
|
+
"test:invite": "pnpm run build && node scripts/invite-selftest.mjs",
|
|
71
|
+
"test:tcp-onion": "pnpm run build && node scripts/tcp-onion-selftest.mjs",
|
|
70
72
|
"smoke:join": "node scripts/smoke-join.mjs",
|
|
71
73
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
72
74
|
"test:express": "pnpm run build && node scripts/express-scheme-selftest.mjs"
|