@decentnetwork/peer 0.1.82 → 0.1.84

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);
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,7 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import nacl from "tweetnacl";
6
6
  import { carrierAddressFromPublicKey, carrierIdFromAddress, carrierIdFromPublicKey, parseCarrierAddress } from "./compat/address.js";
7
7
  import { LegacyBootstrapClient } from "./compat/bootstrap.js";
8
- import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, PACKET_TYPE_BULKMSG, CARRIER_MAX_APP_MESSAGE_LEN, CARRIER_MAX_APP_BULKMSG_LEN, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket, encodeBulkMsgPacket } from "./compat/packet.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
9
  import { NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID, createOnionAnnounceRequest, createOnionDataPacket, createOnionDataRequest, createOnionRequest0, openOnionAnnounceResponse, openOnionDataPacket, openOnionDataResponse } from "./compat/tox-onion.js";
10
10
  import { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
11
11
  import { NET_PACKET_PING_REQUEST, NET_PACKET_PING_RESPONSE, NET_PACKET_GET_NODES, NET_PACKET_SEND_NODES, encodeDhtRpc, decodeDhtRpc, buildPingPlain, parsePingPlain, buildGetNodesPlain, parseGetNodesPlain, buildSendNodesPlain, parseSendNodesNodeBytes, packUdpNodeV4 } from "./compat/dht-rpc.js";
@@ -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
@@ -1125,6 +1129,81 @@ export class Peer {
1125
1129
  }
1126
1130
  await this.sendText(pubkey, envelope);
1127
1131
  }
1132
+ /** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST).
1133
+ * This is the channel the iOS/Android WebRTC SDK uses for call signaling —
1134
+ * each RtcSignal arrives as an "invite" with ext="carrier" and `data` the
1135
+ * reassembled JSON. */
1136
+ onInvite(cb) {
1137
+ this.#events.on("invite", cb);
1138
+ }
1139
+ /** Fired when a peer replies to one of our invites (PACKET_TYPE_INVITE_RESPONSE).
1140
+ * Not used by the one-way WebRTC signaling flow, but surfaced for peers that
1141
+ * do reply. */
1142
+ onInviteResponse(cb) {
1143
+ this.#events.on("inviteResponse", cb);
1144
+ }
1145
+ /**
1146
+ * Send a Carrier friend-invite to a peer — the transport the iOS/Android
1147
+ * Elastos WebRTC SDK listens on (CarrierExtension.registerExtension). Used
1148
+ * for realtime call signaling: pass the RtcSignal JSON as `data` and the
1149
+ * remote's registerExtension handler fires with it.
1150
+ *
1151
+ * Wire-compatible with carrier_invite_friend: sends PACKET_TYPE_INVITE_REQUEST
1152
+ * packets over the reliable message channel, `ext` = "carrier" by default,
1153
+ * fragmented into INVITE_DATA_UNIT (1280-byte) chunks sharing one tid (first
1154
+ * fragment carries totalsz + optional bundle). Signaling is realtime, so this
1155
+ * requires a live session and does NOT fall back to offline express — a call
1156
+ * signal to an unreachable peer should fail fast, not queue.
1157
+ *
1158
+ * @returns the tid used (lets callers correlate a later invite-response).
1159
+ */
1160
+ async sendInvite(pubkey, data, opts = {}) {
1161
+ const friend = this.#friends.get(pubkey);
1162
+ if (!friend) {
1163
+ throw new Error(`Not a friend: ${pubkey}`);
1164
+ }
1165
+ const payload = typeof data === "string" ? new TextEncoder().encode(data) : data;
1166
+ if (payload.length === 0 || payload.length > CARRIER_MAX_INVITE_DATA_LEN) {
1167
+ throw new Error(`invite data must be 1..${CARRIER_MAX_INVITE_DATA_LEN} bytes (got ${payload.length})`);
1168
+ }
1169
+ // `ext: null` sends a plain invite (no extension); default is the "carrier"
1170
+ // extension the native WebRTC SDK registers under.
1171
+ const ext = opts.ext === null ? undefined : (opts.ext ?? CARRIER_EXTENSION_NAME);
1172
+ const bundle = opts.bundle;
1173
+ let session = this.#friendSessions.get(pubkey);
1174
+ if (!session?.established) {
1175
+ await this.#initiateSession(pubkey).catch(() => { });
1176
+ const established = await this.#waitForFriendConnected(pubkey, 5000).catch(() => false);
1177
+ if (established)
1178
+ session = this.#friendSessions.get(pubkey);
1179
+ }
1180
+ if (!session?.established || !session.sessionSharedKey || !session.ourBaseNonce || !(session.remote || session.hasTcpRoute)) {
1181
+ throw new Error(`cannot send invite: no live session to ${pubkey} (peer offline?)`);
1182
+ }
1183
+ // Fragment exactly like carrier_invite_friend: first fragment carries
1184
+ // totalsz and the optional bundle (which counts against the first unit),
1185
+ // the rest carry totalsz=0. bundle is usually absent for WebRTC.
1186
+ const tid = (BigInt(Date.now()) << 20n) ^ BigInt(Math.floor(Math.random() * 0xfffff)) | 1n;
1187
+ const bundleLen = bundle ? new TextEncoder().encode(bundle).length : 0;
1188
+ let off = 0;
1189
+ let first = true;
1190
+ while (off < payload.length) {
1191
+ const unit = first && bundle ? INVITE_DATA_UNIT - bundleLen : INVITE_DATA_UNIT;
1192
+ const end = Math.min(off + unit, payload.length);
1193
+ const p = encodeInviteReqPacket({
1194
+ ext,
1195
+ tid,
1196
+ bundle: first ? bundle : undefined,
1197
+ totalsz: first ? payload.length : 0,
1198
+ data: payload.subarray(off, end)
1199
+ });
1200
+ await this.#sendMessengerPacket(pubkey, PACKET_ID_MESSAGE, p);
1201
+ off = end;
1202
+ first = false;
1203
+ }
1204
+ this.#debugLog(`invite sent to ${pubkey}: ext="${ext ?? ""}" (${payload.length} bytes, tid=${tid})`);
1205
+ return tid;
1206
+ }
1128
1207
  /**
1129
1208
  * Send an application-defined custom packet to a friend. `id` selects the
1130
1209
  * channel and its delivery class by toxcore range: **160–191 lossless**
@@ -2071,6 +2150,39 @@ export class Peer {
2071
2150
  carrier = decodeCarrierPacket(inner);
2072
2151
  }
2073
2152
  catch { /* raw utf-8 peer */ }
2153
+ // Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST/RESPONSE = 34/35)
2154
+ // rides this same PACKET_ID_MESSAGE channel — it's what the iOS/Android
2155
+ // WebRTC SDK uses for call signaling via the "carrier" extension. Large
2156
+ // signals (SDP offers) fragment by tid like bulkmsg; reassemble then
2157
+ // surface as an "invite" event (NOT chat text).
2158
+ if (carrier?.type === PACKET_TYPE_INVITE_REQUEST) {
2159
+ const complete = this.#assembleInvite(friendId, carrier);
2160
+ if (!complete)
2161
+ return; // more fragments pending
2162
+ this.#events.emit("invite", {
2163
+ pubkey: friendId,
2164
+ ext: carrier.ext,
2165
+ bundle: carrier.bundle,
2166
+ data: complete
2167
+ });
2168
+ this.#debugLog(`invite from ${friendId}: ext="${carrier.ext ?? ""}" (${complete.length} bytes)`);
2169
+ return;
2170
+ }
2171
+ if (carrier?.type === PACKET_TYPE_INVITE_RESPONSE) {
2172
+ const complete = carrier.status === 0 ? this.#assembleInvite(friendId, carrier) : new Uint8Array();
2173
+ if (carrier.status === 0 && !complete)
2174
+ return; // more fragments pending
2175
+ this.#events.emit("inviteResponse", {
2176
+ pubkey: friendId,
2177
+ ext: carrier.ext,
2178
+ bundle: carrier.bundle,
2179
+ status: carrier.status,
2180
+ reason: carrier.reason,
2181
+ data: complete ?? new Uint8Array()
2182
+ });
2183
+ this.#debugLog(`invite-response from ${friendId}: status=${carrier.status} ext="${carrier.ext ?? ""}"`);
2184
+ return;
2185
+ }
2074
2186
  if (carrier?.type === PACKET_TYPE_BULKMSG) {
2075
2187
  const complete = this.#assembleBulkMsg(friendId, carrier);
2076
2188
  if (!complete)
@@ -2084,6 +2196,9 @@ export class Peer {
2084
2196
  else {
2085
2197
  text = decodeUtf8Best(inner);
2086
2198
  }
2199
+ // Carrier C peers terminate strings with a NUL — strip it so chat
2200
+ // text doesn't carry an invisible trailing byte.
2201
+ text = text?.replace(/\0+$/u, "");
2087
2202
  if (!text) {
2088
2203
  this.#debugLog(`crypto message packet decode failed from ${friendId}`);
2089
2204
  return;
@@ -3241,12 +3356,50 @@ export class Peer {
3241
3356
  this.#bulkAssembly.delete(key);
3242
3357
  return concatBytes(entry.chunks);
3243
3358
  }
3359
+ /** Append a Carrier invite fragment (INVITE_REQUEST/RESPONSE); returns the
3360
+ * full reassembled payload when the last fragment lands, undefined while
3361
+ * pending. Mirrors #assembleBulkMsg but caps at CARRIER_MAX_INVITE_DATA_LEN
3362
+ * (8192) and uses a separate buffer. Single-fragment invites (the common
3363
+ * case for small WebRTC signals) complete on the first call. */
3364
+ #assembleInvite(friendId, frag) {
3365
+ const now = Date.now();
3366
+ for (const [key, entry] of this.#inviteAssembly) {
3367
+ if (entry.expireAtMs < now)
3368
+ this.#inviteAssembly.delete(key);
3369
+ }
3370
+ const key = `${friendId}:${frag.tid.toString()}`;
3371
+ let entry = this.#inviteAssembly.get(key);
3372
+ if (!entry) {
3373
+ // First fragment carries totalsz. A lone fragment whose data already
3374
+ // equals totalsz completes immediately below.
3375
+ if (!frag.totalsz || frag.totalsz > CARRIER_MAX_INVITE_DATA_LEN) {
3376
+ this.#debugLog(`invite from ${friendId} with invalid/missing totalsz ${frag.totalsz} — dropped`);
3377
+ return undefined;
3378
+ }
3379
+ entry = { total: frag.totalsz, chunks: [], got: 0, expireAtMs: now + 60_000 };
3380
+ this.#inviteAssembly.set(key, entry);
3381
+ }
3382
+ if (frag.data.length === 0 || entry.got + frag.data.length > entry.total) {
3383
+ this.#debugLog(`invite fragment from ${friendId} overflows totalsz — dropped`);
3384
+ this.#inviteAssembly.delete(key);
3385
+ return undefined;
3386
+ }
3387
+ entry.chunks.push(frag.data);
3388
+ entry.got += frag.data.length;
3389
+ if (entry.got < entry.total)
3390
+ return undefined;
3391
+ this.#inviteAssembly.delete(key);
3392
+ return concatBytes(entry.chunks);
3393
+ }
3244
3394
  /** iOS Beagle sends files inline as a JSON envelope over (bulk)messages:
3245
3395
  * {"data":"<base64>","fileExtension":".jpg","fileName":"…","type":"image"}.
3246
3396
  * Detect it, decode, and emit an inlineFile event instead of dumping
3247
3397
  * base64 into the chat. Returns true when handled. */
3248
3398
  #tryEmitInlineFile(friendId, text, via) {
3249
- const trimmed = text.trimStart();
3399
+ // The Carrier C SDK sends messages with a trailing C-string NUL, which
3400
+ // JSON.parse rejects. iOS's own decoder strips NULs before parsing
3401
+ // (CodableUtil.decode: replacingOccurrences(of: "\0")) — mirror that.
3402
+ const trimmed = text.replaceAll("\0", "").trim();
3250
3403
  if (!trimmed.startsWith("{") || !trimmed.includes("\"fileName\"") || !trimmed.includes("\"data\"")) {
3251
3404
  return false;
3252
3405
  }
@@ -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.82",
3
+ "version": "0.1.84",
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,7 @@
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",
70
71
  "smoke:join": "node scripts/smoke-join.mjs",
71
72
  "typecheck": "tsc -p tsconfig.json --noEmit",
72
73
  "test:express": "pnpm run build && node scripts/express-scheme-selftest.mjs"