@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.
@@ -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
- export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessagePacket | BulkMsgPacket;
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;
@@ -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). */
@@ -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
  }): {
@@ -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) {
@@ -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.83",
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"