@decentnetwork/peer 0.1.79 → 0.1.82

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.
@@ -28,6 +28,7 @@ export class LegacyExpressClient {
28
28
  return {
29
29
  host: node.host,
30
30
  port: node.port,
31
+ tls: node.tls !== false, // default HTTPS; opt into HTTP with tls:false
31
32
  sharedKey: nacl.box.before(expressPk, this.#selfKeyPair.secretKey)
32
33
  };
33
34
  });
@@ -152,7 +153,8 @@ export class LegacyExpressClient {
152
153
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
153
154
  }
154
155
  async #http(node, method, path, body) {
155
- const url = `https://${node.host}:${node.port}/${path}`;
156
+ const scheme = node.tls ? "https" : "http";
157
+ const url = `${scheme}://${node.host}:${node.port}/${path}`;
156
158
  const controller = new AbortController();
157
159
  const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
158
160
  try {
@@ -80,5 +80,9 @@ export declare function openCryptoDataPacket(packet: Uint8Array, opts: {
80
80
  bufferStart: number;
81
81
  packetNumber: number;
82
82
  nonceLast2: number;
83
+ /** The exact nonce this packet decrypted with. The caller must track
84
+ * recvBaseNonce = usedNonce (+1 for the next expected) — NOT overwrite
85
+ * just the low two bytes, which loses the carry into byte 21. */
86
+ usedNonce: Uint8Array;
83
87
  } | undefined;
84
88
  export declare function incrementNonce(nonce: Uint8Array): void;
@@ -191,9 +191,27 @@ export function openCryptoDataPacket(packet, opts) {
191
191
  if (packet.length <= CRYPTO_DATA_PACKET_MIN_SIZE || packet[0] !== NET_PACKET_CRYPTO_DATA) {
192
192
  return undefined;
193
193
  }
194
+ // The wire carries only the low 16 bits of the sender's nonce counter.
195
+ // Reconstruct the full nonce like toxcore handle_data_packet: advance a
196
+ // COPY of our tracked nonce by the uint16 difference WITH CARRY into the
197
+ // upper bytes. The old code overwrote the two low bytes in place, which
198
+ // silently dropped the carry whenever packet loss spanned a 64k nonce
199
+ // wrap — from then on EVERY packet failed to decrypt until re-handshake
200
+ // (observed as endless "no session matched" from high-rate peers, whose
201
+ // counters wrap every ~30-60s of CCTV/file traffic).
194
202
  const nonce = opts.recvBaseNonce.slice();
195
- nonce[nonce.length - 2] = packet[1];
196
- nonce[nonce.length - 1] = packet[2];
203
+ const trackedLow = ((nonce[nonce.length - 2] << 8) | nonce[nonce.length - 1]) >>> 0;
204
+ const wireLow = ((packet[1] << 8) | packet[2]) >>> 0;
205
+ const diff = (wireLow - trackedLow) & 0xffff;
206
+ if (diff < 0x8000) {
207
+ addToNonceCounter(nonce, diff);
208
+ }
209
+ else {
210
+ // The wire counter is BEHIND our tracked one — a reordered/duplicate
211
+ // packet from earlier in the stream (dual-path UDP+relay delivery
212
+ // reorders constantly). Step backward with borrow.
213
+ subFromNonceCounter(nonce, 0x10000 - diff);
214
+ }
197
215
  const cipher = packet.slice(3);
198
216
  const plain = nacl.secretbox.open(cipher, nonce, opts.sessionSharedKey);
199
217
  if (!plain || plain.length <= CRYPTO_DATA_PLAIN_HEADER_LENGTH) {
@@ -212,9 +230,29 @@ export function openCryptoDataPacket(packet, opts) {
212
230
  payload: plain.slice(payloadOffset),
213
231
  bufferStart,
214
232
  packetNumber,
215
- nonceLast2: ((packet[1] << 8) | packet[2]) >>> 0
233
+ nonceLast2: ((packet[1] << 8) | packet[2]) >>> 0,
234
+ usedNonce: nonce
216
235
  };
217
236
  }
237
+ /** Add `count` to the big-endian nonce counter with carry (toxcore
238
+ * increment_nonce_number). */
239
+ function addToNonceCounter(nonce, count) {
240
+ let carry = count >>> 0;
241
+ for (let i = nonce.length - 1; i >= 0 && carry > 0; i--) {
242
+ const sum = nonce[i] + (carry & 0xff);
243
+ nonce[i] = sum & 0xff;
244
+ carry = (carry >>> 8) + (sum > 0xff ? 1 : 0);
245
+ }
246
+ }
247
+ /** Subtract `count` from the big-endian nonce counter with borrow. */
248
+ function subFromNonceCounter(nonce, count) {
249
+ let borrow = count >>> 0;
250
+ for (let i = nonce.length - 1; i >= 0 && borrow > 0; i--) {
251
+ const sub = nonce[i] - (borrow & 0xff);
252
+ nonce[i] = sub & 0xff;
253
+ borrow = (borrow >>> 8) + (sub < 0 ? 1 : 0);
254
+ }
255
+ }
218
256
  function createCookie(opts) {
219
257
  const timestamp = BigInt(Math.floor(Date.now() / 1000));
220
258
  const contents = concatBytes([
@@ -1,6 +1,9 @@
1
1
  export declare const PACKET_TYPE_USERINFO = 3;
2
2
  export declare const PACKET_TYPE_FRIEND_REQUEST = 6;
3
3
  export declare const PACKET_TYPE_MESSAGE = 33;
4
+ export declare const PACKET_TYPE_BULKMSG = 36;
5
+ export declare const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
6
+ export declare const CARRIER_MAX_APP_BULKMSG_LEN: number;
4
7
  export type UserInfoPacket = {
5
8
  type: typeof PACKET_TYPE_USERINFO;
6
9
  avatar: boolean;
@@ -22,7 +25,16 @@ export type FriendMessagePacket = {
22
25
  ext?: string;
23
26
  data: Uint8Array;
24
27
  };
25
- export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessagePacket;
28
+ export type BulkMsgPacket = {
29
+ type: typeof PACKET_TYPE_BULKMSG;
30
+ ext?: string;
31
+ /** Full message length — set on the FIRST fragment only, 0 on the rest. */
32
+ totalsz: number;
33
+ /** Shared id linking all fragments of one message. */
34
+ tid: bigint;
35
+ data: Uint8Array;
36
+ };
37
+ export type CarrierPacket = UserInfoPacket | FriendRequestPacket | FriendMessagePacket | BulkMsgPacket;
26
38
  /**
27
39
  * Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
28
40
  * this every time the local user's name / status message / profile fields
@@ -52,4 +64,15 @@ export declare function encodeFriendRequestPacket(opts: {
52
64
  hello?: string;
53
65
  }): Uint8Array;
54
66
  export declare function encodeFriendMessagePacket(data: Uint8Array | string, ext?: string): Uint8Array;
67
+ /**
68
+ * Encode one Carrier `bulkmsg` fragment (PACKET_TYPE_BULKMSG = 36). Field
69
+ * layout per src/carrier/packet.fbs: 0=ext string, 1=totalsz uint,
70
+ * 2=tid long, 3=data [ubyte]. Mirrors the C SDK's send_bulk_message.
71
+ */
72
+ export declare function encodeBulkMsgPacket(opts: {
73
+ ext?: string;
74
+ totalsz: number;
75
+ tid: bigint;
76
+ data: Uint8Array;
77
+ }): Uint8Array;
55
78
  export declare function decodeCarrierPacket(bytes: Uint8Array): CarrierPacket;
@@ -2,12 +2,20 @@ import { Builder, ByteBuffer, Encoding } from "flatbuffers";
2
2
  export const PACKET_TYPE_USERINFO = 3;
3
3
  export const PACKET_TYPE_FRIEND_REQUEST = 6;
4
4
  export const PACKET_TYPE_MESSAGE = 33;
5
+ export const PACKET_TYPE_BULKMSG = 36;
6
+ // Carrier C SDK message-size constants (carrier.h). A single friendmsg may
7
+ // carry at most 1024 bytes; anything larger is split into BULKMSG fragments
8
+ // of <=1024 raw bytes sharing one tid (totalsz set on the FIRST fragment
9
+ // only), reassembled in arrival order by the receiver.
10
+ export const CARRIER_MAX_APP_MESSAGE_LEN = 1024;
11
+ export const CARRIER_MAX_APP_BULKMSG_LEN = 5 * 1024 * 1024;
5
12
  // Body type indices into FlatBuffers `anybody` union (carrier/packet.fbs).
6
13
  // The .fbs declares: userinfo, friendreq, friendmsg, invitereq, invitersp,
7
14
  // bulkmsg — flatbuffers maps those to 1, 2, 3, 4, 5, 6 (NONE = 0).
8
15
  const ANYBODY_USERINFO = 1;
9
16
  const ANYBODY_FRIENDREQ = 2;
10
17
  const ANYBODY_FRIENDMSG = 3;
18
+ const ANYBODY_BULKMSG = 6;
11
19
  /**
12
20
  * Encode a Carrier `userinfo` packet (FB type 3). The Carrier C SDK sends
13
21
  * this every time the local user's name / status message / profile fields
@@ -69,6 +77,29 @@ export function encodeFriendMessagePacket(data, ext) {
69
77
  const friendMsg = builder.endObject();
70
78
  return finishCarrierPacket(builder, PACKET_TYPE_MESSAGE, ANYBODY_FRIENDMSG, friendMsg);
71
79
  }
80
+ /**
81
+ * Encode one Carrier `bulkmsg` fragment (PACKET_TYPE_BULKMSG = 36). Field
82
+ * layout per src/carrier/packet.fbs: 0=ext string, 1=totalsz uint,
83
+ * 2=tid long, 3=data [ubyte]. Mirrors the C SDK's send_bulk_message.
84
+ */
85
+ export function encodeBulkMsgPacket(opts) {
86
+ const builder = new Builder(opts.data.length + 128);
87
+ const dataOffset = builder.createByteVector(opts.data);
88
+ const extOffset = opts.ext ? builder.createString(opts.ext) : 0;
89
+ builder.startObject(4);
90
+ builder.addFieldOffset(3, dataOffset, 0);
91
+ if (opts.tid !== 0n) {
92
+ builder.addFieldInt64(2, opts.tid, BigInt(0));
93
+ }
94
+ if (opts.totalsz) {
95
+ builder.addFieldInt32(1, opts.totalsz, 0);
96
+ }
97
+ if (extOffset) {
98
+ builder.addFieldOffset(0, extOffset, 0);
99
+ }
100
+ const bulk = builder.endObject();
101
+ return finishCarrierPacket(builder, PACKET_TYPE_BULKMSG, ANYBODY_BULKMSG, bulk);
102
+ }
72
103
  export function decodeCarrierPacket(bytes) {
73
104
  const bb = new ByteBuffer(bytes);
74
105
  const root = bb.readInt32(bb.position()) + bb.position();
@@ -106,6 +137,15 @@ export function decodeCarrierPacket(bytes) {
106
137
  data: readByteVectorField(body, 1) ?? new Uint8Array()
107
138
  };
108
139
  }
140
+ if (type === PACKET_TYPE_BULKMSG && bodyType === ANYBODY_BULKMSG) {
141
+ return {
142
+ type,
143
+ ext: readStringField(body, 0) ?? undefined,
144
+ totalsz: readUint32Field(body, 1),
145
+ tid: readInt64Field(body, 2),
146
+ data: readByteVectorField(body, 3) ?? new Uint8Array()
147
+ };
148
+ }
109
149
  throw new Error(`unsupported carrier packet type/body: ${type}/${bodyType}`);
110
150
  }
111
151
  function finishCarrierPacket(builder, type, bodyType, bodyOffset) {
@@ -125,6 +165,14 @@ function readUint8Field(table, field) {
125
165
  const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
126
166
  return offset ? table.bb.readUint8(table.bb_pos + offset) : 0;
127
167
  }
168
+ function readUint32Field(table, field) {
169
+ const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
170
+ return offset ? table.bb.readUint32(table.bb_pos + offset) : 0;
171
+ }
172
+ function readInt64Field(table, field) {
173
+ const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
174
+ return offset ? table.bb.readInt64(table.bb_pos + offset) : BigInt(0);
175
+ }
128
176
  function readStringField(table, field) {
129
177
  const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
130
178
  if (!offset) {
@@ -23,7 +23,17 @@ import { EventEmitter } from "node:events";
23
23
  import { base58ToBytes } from "../utils/base58.js";
24
24
  import { TcpRelayClient } from "./tcp-relay.js";
25
25
  const KEY_SIZE = 32;
26
- const MAX_RELAY_CONNECTIONS = 3;
26
+ // Connect to (up to) this many bootstrap nodes as persistent TCP relays. It was
27
+ // 3 — but the pool picks the FIRST N in order, so a peer only met friends whose
28
+ // relays happened to be in its own top-3. A NATIVE peer (iOS/Android) reaches us
29
+ // ONLY over a relay we both sit on (OOB cookie handshake); if our top-3 doesn't
30
+ // include the relay it uses, it's stuck "connecting" forever — even though we
31
+ // share the node further down the list (observed: iOS uses sh.callt.net =
32
+ // 47.100.103.201 as its #1 relay, which sat at #7 in our bootstrap so we never
33
+ // connected to it). Covering more of the shared bootstrap set makes the same-
34
+ // relay rendezvous reliable for native peers. Cheap: a handful of extra idle TCP
35
+ // keepalives.
36
+ const MAX_RELAY_CONNECTIONS = 8;
27
37
  const RECONNECT_BASE_MS = 5_000;
28
38
  const RECONNECT_MAX_MS = 120_000;
29
39
  export class TcpRelayPool extends EventEmitter {
package/dist/peer.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type BootstrapResult } from "./compat/bootstrap.js";
2
2
  import type { FriendRecord } from "./store/friends.js";
3
- import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
3
+ import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, LookupResult, NetworkNode, PeerOptions, TextMessage } from "./types/peer.js";
4
4
  export declare class Peer {
5
5
  #private;
6
6
  private constructor();
@@ -82,6 +82,23 @@ export declare class Peer {
82
82
  waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>;
83
83
  onFriendRequest(cb: (req: FriendRequest) => void): void;
84
84
  onText(cb: (msg: TextMessage) => void): void;
85
+ /** Files received inline over (bulk)messages — the iOS/C Carrier apps'
86
+ * native way of sending images/audio online (FileModel JSON envelope). */
87
+ onInlineFile(cb: (evt: InlineFileEvent) => void): void;
88
+ /**
89
+ * Send a file INLINE over the message channel the way iOS/C Carrier apps
90
+ * do: a FileModel JSON envelope ({data: base64, fileExtension, fileName,
91
+ * type}) carried as a (bulk)message. This is the ONLY file path a native
92
+ * Beagle client can receive online — the toxcore file transfer (80-82)
93
+ * used by sendFile() is invisible to the Carrier C SDK. Best for images
94
+ * and small files; the hard protocol cap is ~5MB (base64-inflated), keep
95
+ * real use well below it.
96
+ */
97
+ sendInlineFile(pubkey: string, opts: {
98
+ name: string;
99
+ data: Uint8Array;
100
+ fileType?: "image" | "audio" | "text" | "unknown";
101
+ }): Promise<void>;
85
102
  /**
86
103
  * Send an application-defined custom packet to a friend. `id` selects the
87
104
  * channel and its delivery class by toxcore range: **160–191 lossless**