@peers-app/peers-sdk 0.20.5 → 0.21.0

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.
Files changed (33) hide show
  1. package/dist/data/devices.d.ts +4 -4
  2. package/dist/data/voice-messages.d.ts +2 -2
  3. package/dist/device/binary-peer-connection-v2.d.ts +19 -2
  4. package/dist/device/binary-peer-connection-v2.js +127 -20
  5. package/dist/device/binary-peer-connection-v2.test.js +60 -0
  6. package/dist/device/binary-peer-connection.d.ts +13 -2
  7. package/dist/device/binary-peer-connection.js +77 -14
  8. package/dist/device/binary-peer-connection.test.js +21 -4
  9. package/dist/device/connection.d.ts +1 -1
  10. package/dist/device/device.d.ts +1 -1
  11. package/dist/device-pairing/constants.d.ts +34 -0
  12. package/dist/device-pairing/constants.js +37 -0
  13. package/dist/device-pairing/crypto.d.ts +45 -0
  14. package/dist/device-pairing/crypto.js +135 -0
  15. package/dist/device-pairing/device-pairing.test.d.ts +1 -0
  16. package/dist/device-pairing/device-pairing.test.js +174 -0
  17. package/dist/device-pairing/index.d.ts +6 -0
  18. package/dist/device-pairing/index.js +22 -0
  19. package/dist/device-pairing/invitation.d.ts +13 -0
  20. package/dist/device-pairing/invitation.js +85 -0
  21. package/dist/device-pairing/signaling.d.ts +9 -0
  22. package/dist/device-pairing/signaling.js +29 -0
  23. package/dist/device-pairing/transcript.d.ts +7 -0
  24. package/dist/device-pairing/transcript.js +38 -0
  25. package/dist/device-pairing/types.d.ts +1023 -0
  26. package/dist/device-pairing/types.js +304 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/rpc-types.d.ts +8 -0
  30. package/dist/rpc-types.js +7 -0
  31. package/dist/types/peer-device.d.ts +38 -1
  32. package/dist/types/peer-device.js +1 -0
  33. package/package.json +1 -1
@@ -49,15 +49,15 @@ export declare const deviceInfoSchema: z.ZodObject<{
49
49
  publicBoxKey: string;
50
50
  userId: string;
51
51
  deviceId: string;
52
- userName?: string | undefined;
53
52
  deviceName?: string | undefined;
53
+ userName?: string | undefined;
54
54
  }, {
55
55
  publicKey: string;
56
56
  publicBoxKey: string;
57
57
  userId: string;
58
58
  deviceId: string;
59
- userName?: string | undefined;
60
59
  deviceName?: string | undefined;
60
+ userName?: string | undefined;
61
61
  }>;
62
62
  /** Signed identity and optional presentation hints advertised by a device. */
63
63
  export type IDeviceInfo = z.infer<typeof deviceInfoSchema>;
@@ -81,8 +81,8 @@ export declare const deviceHandshakeSchema: z.ZodObject<{
81
81
  deviceId: string;
82
82
  connectionId: string;
83
83
  serverAddress: string;
84
- userName?: string | undefined;
85
84
  deviceName?: string | undefined;
85
+ userName?: string | undefined;
86
86
  }, {
87
87
  timestamp: number;
88
88
  publicKey: string;
@@ -91,8 +91,8 @@ export declare const deviceHandshakeSchema: z.ZodObject<{
91
91
  deviceId: string;
92
92
  connectionId: string;
93
93
  serverAddress: string;
94
- userName?: string | undefined;
95
94
  deviceName?: string | undefined;
95
+ userName?: string | undefined;
96
96
  }>;
97
97
  /** Signed device information bound to one connection handshake. */
98
98
  export type IDeviceHandshake = z.infer<typeof deviceHandshakeSchema>;
@@ -10,9 +10,9 @@ export declare const voiceMessageSchema: z.ZodObject<{
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  content: string;
12
12
  role: "user" | "assistant";
13
+ sessionId: string;
13
14
  createdAt: Date;
14
15
  voiceMessageId: string;
15
- sessionId: string;
16
16
  threadId?: string | undefined;
17
17
  }, {
18
18
  content: string;
@@ -26,8 +26,8 @@ export type IVoiceMessage = z.infer<typeof voiceMessageSchema>;
26
26
  export declare function VoiceMessages(dataContext?: DataContext): import("..").Table<{
27
27
  content: string;
28
28
  role: "user" | "assistant";
29
+ sessionId: string;
29
30
  createdAt: Date;
30
31
  voiceMessageId: string;
31
- sessionId: string;
32
32
  threadId?: string | undefined;
33
33
  }>;
@@ -37,6 +37,21 @@ export interface IBinaryPeer {
37
37
  }
38
38
  /** Backwards-compatible name for a WebRTC binary peer. */
39
39
  export type IWebRTCPeer = IBinaryPeer;
40
+ /** Hard limits applied before allocating or decoding inbound binary messages. */
41
+ export interface BinaryPeerReceiveLimits {
42
+ /** Maximum encoded bytes in one complete RPC message. */
43
+ maxRpcMessageBytes: number;
44
+ /** Maximum bytes in one reassembled raw stream message. */
45
+ maxStreamMessageBytes: number;
46
+ /** Maximum bytes reserved across all incomplete inbound messages. */
47
+ maxInFlightReassemblyBytes: number;
48
+ /** Maximum number of incomplete inbound messages retained at once. */
49
+ maxConcurrentReassemblies: number;
50
+ /** Maximum time an incomplete inbound message may retain memory. */
51
+ reassemblyTimeoutMs: number;
52
+ }
53
+ /** General-purpose receive limits for authenticated Peers connections. */
54
+ export declare const DEFAULT_BINARY_PEER_RECEIVE_LIMITS: Readonly<BinaryPeerReceiveLimits>;
40
55
  /** Options for wrapping a binary transport in a verified Peers connection. */
41
56
  export interface WrapBinaryPeerOptions {
42
57
  /** Protocol identifier used for server address (e.g., 'wrtc' or 'ws') */
@@ -46,12 +61,14 @@ export interface WrapBinaryPeerOptions {
46
61
  * When true, sets secureLocal and secureRemote to skip application-level encryption.
47
62
  */
48
63
  markTransportSecure?: boolean;
64
+ /** Optional hard limits for untrusted inbound messages. */
65
+ receiveLimits?: Partial<BinaryPeerReceiveLimits>;
49
66
  }
50
67
  /**
51
68
  * Creates an ISocket from a binary peer. This is the core v2 factory function.
52
69
  * Replaces both createBinaryPeerSocket (v1) and StreamedSocket.
53
70
  */
54
- export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string): ISocket;
71
+ export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string, receiveLimitOverrides?: Partial<BinaryPeerReceiveLimits>): ISocket;
55
72
  /**
56
73
  * Wraps a binary peer (WebRTC or WebSocket) into a Connection.
57
74
  * This is the main implementation that handles all binary protocols.
@@ -61,5 +78,5 @@ export declare function wrapBinaryPeer(connectionId: string, peer: IBinaryPeer,
61
78
  * Wraps a WebRTC peer into a Connection.
62
79
  * WebRTC is encrypted at the transport layer (DTLS-SRTP).
63
80
  */
64
- export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn): Connection;
81
+ export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn, receiveLimits?: Partial<BinaryPeerReceiveLimits>): Connection;
65
82
  export {};
@@ -17,6 +17,7 @@
17
17
  * and event-loop yielding (critical for Go sidecar WebRTC pipeline).
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.DEFAULT_BINARY_PEER_RECEIVE_LIMITS = void 0;
20
21
  exports.createBinaryPeerSocket = createBinaryPeerSocket;
21
22
  exports.wrapBinaryPeer = wrapBinaryPeer;
22
23
  exports.wrapWrtc = wrapWrtc;
@@ -35,6 +36,14 @@ const RPC_CONT = 0x02; // [0x02][4B msgId][chunk data]
35
36
  const STREAM_COMPLETE = 0x10; // [0x10][1B sidLen][streamId bytes][data]
36
37
  const STREAM_START = 0x11; // [0x11][1B sidLen][streamId bytes][4B totalSize][chunk data]
37
38
  const STREAM_CONT = 0x12; // [0x12][1B sidLen][streamId bytes][chunk data]
39
+ /** General-purpose receive limits for authenticated Peers connections. */
40
+ exports.DEFAULT_BINARY_PEER_RECEIVE_LIMITS = Object.freeze({
41
+ maxRpcMessageBytes: 256 * 1024 * 1024,
42
+ maxStreamMessageBytes: 256 * 1024 * 1024,
43
+ maxInFlightReassemblyBytes: 512 * 1024 * 1024,
44
+ maxConcurrentReassemblies: 64,
45
+ reassemblyTimeoutMs: 30_000,
46
+ });
38
47
  const WRTC_CONFIG = {
39
48
  maxFramePayload: 64 * 1024, // 64KB - safe for SCTP
40
49
  highWaterMark: 128 * 1024, // 128KB
@@ -227,7 +236,7 @@ function buildStreamCont(sidBytes, chunkData) {
227
236
  * Creates an ISocket from a binary peer. This is the core v2 factory function.
228
237
  * Replaces both createBinaryPeerSocket (v1) and StreamedSocket.
229
238
  */
230
- function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
239
+ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receiveLimitOverrides = {}) {
231
240
  const handlers = {};
232
241
  const callbacks = {};
233
242
  const rawBytesHandlers = {};
@@ -235,6 +244,15 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
235
244
  const stats = (0, socket_type_1.createSocketStats)();
236
245
  const transportType = protocol === "wrtc" ? "wrtc" : protocol === "ws" ? "ws" : "unknown";
237
246
  const config = getTransportConfig(protocol);
247
+ const receiveLimits = {
248
+ ...exports.DEFAULT_BINARY_PEER_RECEIVE_LIMITS,
249
+ ...receiveLimitOverrides,
250
+ };
251
+ for (const [name, value] of Object.entries(receiveLimits)) {
252
+ if (!Number.isSafeInteger(value) || value <= 0) {
253
+ throw new Error(`Invalid binary peer receive limit ${name}: ${value}`);
254
+ }
255
+ }
238
256
  // ─── Drain / Backpressure ────────────────────────────────────────
239
257
  // Single persistent drain handler to avoid leaking listeners
240
258
  let drainResolve = null;
@@ -311,8 +329,56 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
311
329
  rpcSendQueue = rpcSendQueue.then(sendChunked, sendChunked);
312
330
  return rpcSendQueue;
313
331
  }
314
- // RPC reassembly state: keyed by msgId
315
332
  const rpcReassemblyBuffers = new Map();
333
+ let inFlightReassemblyBytes = 0;
334
+ function clearReassemblyState(state) {
335
+ clearTimeout(state.timer);
336
+ inFlightReassemblyBytes -= state.buffer.length;
337
+ }
338
+ function clearAllReassemblies() {
339
+ for (const state of rpcReassemblyBuffers.values()) {
340
+ clearTimeout(state.timer);
341
+ }
342
+ rpcReassemblyBuffers.clear();
343
+ for (const state of streamReassemblyBuffers.values()) {
344
+ clearTimeout(state.timer);
345
+ }
346
+ streamReassemblyBuffers.clear();
347
+ inFlightReassemblyBytes = 0;
348
+ }
349
+ function closeForProtocolViolation(reason) {
350
+ if (isClosed) {
351
+ return;
352
+ }
353
+ console.warn(`Closing ${protocol} peer after binary protocol violation: ${reason}`);
354
+ isClosed = true;
355
+ clearAllReassemblies();
356
+ socket.connected = false;
357
+ peer.destroy();
358
+ }
359
+ function reserveReassembly(key, totalSize, maximumMessageBytes, onTimeout) {
360
+ const concurrentCount = rpcReassemblyBuffers.size + streamReassemblyBuffers.size;
361
+ if (totalSize > maximumMessageBytes) {
362
+ closeForProtocolViolation(`${key} exceeds the message-size limit`);
363
+ return undefined;
364
+ }
365
+ if (concurrentCount >= receiveLimits.maxConcurrentReassemblies) {
366
+ closeForProtocolViolation("too many concurrent message reassemblies");
367
+ return undefined;
368
+ }
369
+ if (inFlightReassemblyBytes + totalSize > receiveLimits.maxInFlightReassemblyBytes) {
370
+ closeForProtocolViolation("aggregate reassembly memory limit exceeded");
371
+ return undefined;
372
+ }
373
+ const state = {
374
+ buffer: new Uint8Array(totalSize),
375
+ offset: 0,
376
+ timer: setTimeout(onTimeout, receiveLimits.reassemblyTimeoutMs),
377
+ };
378
+ state.timer.unref?.();
379
+ inFlightReassemblyBytes += totalSize;
380
+ return state;
381
+ }
316
382
  function handleIncomingRPC(bytes) {
317
383
  const message = decodeMessage(bytes);
318
384
  if (message.type === "callback") {
@@ -407,6 +473,10 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
407
473
  // ── RPC frames ──────────────────────────────────────────
408
474
  case RPC_COMPLETE: {
409
475
  const rpcBytes = bytes.subarray(1);
476
+ if (rpcBytes.length > receiveLimits.maxRpcMessageBytes) {
477
+ closeForProtocolViolation("complete RPC exceeds the message-size limit");
478
+ return;
479
+ }
410
480
  handleIncomingRPC(rpcBytes);
411
481
  break;
412
482
  }
@@ -417,14 +487,22 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
417
487
  const msgId = view.getUint32(1, false);
418
488
  const totalSize = view.getUint32(5, false);
419
489
  const chunkData = bytes.subarray(9);
420
- const buffer = new Uint8Array(totalSize);
421
- buffer.set(chunkData, 0);
422
- const offset = chunkData.length;
423
- if (offset >= totalSize) {
424
- handleIncomingRPC(buffer);
490
+ if (totalSize === 0 || chunkData.length > totalSize || rpcReassemblyBuffers.has(msgId)) {
491
+ closeForProtocolViolation("invalid or duplicate RPC start frame");
492
+ return;
493
+ }
494
+ const state = reserveReassembly(`RPC ${msgId}`, totalSize, receiveLimits.maxRpcMessageBytes, () => closeForProtocolViolation(`RPC ${msgId} reassembly timed out`));
495
+ if (!state) {
496
+ return;
497
+ }
498
+ state.buffer.set(chunkData, 0);
499
+ state.offset = chunkData.length;
500
+ if (state.offset === totalSize) {
501
+ clearReassemblyState(state);
502
+ handleIncomingRPC(state.buffer);
425
503
  }
426
504
  else {
427
- rpcReassemblyBuffers.set(msgId, { buffer, offset });
505
+ rpcReassemblyBuffers.set(msgId, state);
428
506
  }
429
507
  break;
430
508
  }
@@ -437,10 +515,15 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
437
515
  const state = rpcReassemblyBuffers.get(msgId);
438
516
  if (!state)
439
517
  return; // no START received, discard
518
+ if (chunkData.length > state.buffer.length - state.offset) {
519
+ closeForProtocolViolation(`RPC ${msgId} continuation exceeds declared size`);
520
+ return;
521
+ }
440
522
  state.buffer.set(chunkData, state.offset);
441
523
  state.offset += chunkData.length;
442
- if (state.offset >= state.buffer.length) {
524
+ if (state.offset === state.buffer.length) {
443
525
  rpcReassemblyBuffers.delete(msgId);
526
+ clearReassemblyState(state);
444
527
  handleIncomingRPC(state.buffer);
445
528
  }
446
529
  break;
@@ -455,6 +538,10 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
455
538
  const sidBytes = bytes.subarray(2, 2 + sidLen);
456
539
  const streamId = decodeStreamId(sidBytes);
457
540
  const payload = bytes.subarray(2 + sidLen);
541
+ if (payload.length > receiveLimits.maxStreamMessageBytes) {
542
+ closeForProtocolViolation("complete stream message exceeds the message-size limit");
543
+ return;
544
+ }
458
545
  const handler = rawBytesHandlers[streamId];
459
546
  if (handler)
460
547
  handler(payload);
@@ -471,16 +558,26 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
471
558
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
472
559
  const totalSize = view.getUint32(2 + sidLen, false);
473
560
  const chunkData = bytes.subarray(2 + sidLen + 4);
474
- const buffer = new Uint8Array(totalSize);
475
- buffer.set(chunkData, 0);
476
- const offset = chunkData.length;
477
- if (offset >= totalSize) {
561
+ if (totalSize === 0 ||
562
+ chunkData.length > totalSize ||
563
+ streamReassemblyBuffers.has(streamId)) {
564
+ closeForProtocolViolation("invalid or duplicate stream start frame");
565
+ return;
566
+ }
567
+ const state = reserveReassembly(`stream ${streamId}`, totalSize, receiveLimits.maxStreamMessageBytes, () => closeForProtocolViolation(`stream ${streamId} reassembly timed out`));
568
+ if (!state) {
569
+ return;
570
+ }
571
+ state.buffer.set(chunkData, 0);
572
+ state.offset = chunkData.length;
573
+ if (state.offset === totalSize) {
574
+ clearReassemblyState(state);
478
575
  const handler = rawBytesHandlers[streamId];
479
576
  if (handler)
480
- handler(buffer);
577
+ handler(state.buffer);
481
578
  }
482
579
  else {
483
- streamReassemblyBuffers.set(streamId, { buffer, offset });
580
+ streamReassemblyBuffers.set(streamId, state);
484
581
  }
485
582
  break;
486
583
  }
@@ -496,10 +593,15 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
496
593
  const state = streamReassemblyBuffers.get(streamId);
497
594
  if (!state)
498
595
  return; // no START received, discard
596
+ if (chunkData.length > state.buffer.length - state.offset) {
597
+ closeForProtocolViolation(`stream ${streamId} continuation exceeds declared size`);
598
+ return;
599
+ }
499
600
  state.buffer.set(chunkData, state.offset);
500
601
  state.offset += chunkData.length;
501
- if (state.offset >= state.buffer.length) {
602
+ if (state.offset === state.buffer.length) {
502
603
  streamReassemblyBuffers.delete(streamId);
604
+ clearReassemblyState(state);
503
605
  const handler = rawBytesHandlers[streamId];
504
606
  if (handler)
505
607
  handler(state.buffer);
@@ -512,6 +614,10 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
512
614
  // legacy format (no discriminator), try to decode as RPC.
513
615
  // This handles the transition period where v1 peers send raw RPC without
514
616
  // a frame type byte.
617
+ if (bytes.length > receiveLimits.maxRpcMessageBytes) {
618
+ closeForProtocolViolation("legacy RPC exceeds the message-size limit");
619
+ return;
620
+ }
515
621
  handleIncomingRPC(bytes);
516
622
  break;
517
623
  }
@@ -526,6 +632,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
526
632
  if (isClosed)
527
633
  return;
528
634
  isClosed = true;
635
+ clearAllReassemblies();
529
636
  socket.connected = false;
530
637
  });
531
638
  // ─── ISocket Implementation ──────────────────────────────────────
@@ -574,8 +681,8 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
574
681
  * This is the main implementation that handles all binary protocols.
575
682
  */
576
683
  function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, getTrustLevel) {
577
- const { protocol, markTransportSecure } = options;
578
- const socket = createBinaryPeerSocket(connectionId, peer, protocol);
684
+ const { protocol, markTransportSecure, receiveLimits } = options;
685
+ const socket = createBinaryPeerSocket(connectionId, peer, protocol, receiveLimits);
579
686
  const serverAddress = `${protocol}://${connectionId}`;
580
687
  const localServerAddresses = initiator ? undefined : [serverAddress];
581
688
  if (process.env.NODE_ENV !== "test") {
@@ -592,6 +699,6 @@ function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, get
592
699
  * Wraps a WebRTC peer into a Connection.
593
700
  * WebRTC is encrypted at the transport layer (DTLS-SRTP).
594
701
  */
595
- function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel) {
596
- return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", markTransportSecure: true }, getTrustLevel);
702
+ function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel, receiveLimits) {
703
+ return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", markTransportSecure: true, receiveLimits }, getTrustLevel);
597
704
  }
@@ -11,6 +11,7 @@ class MockBinaryPeer {
11
11
  drainHandlers = [];
12
12
  otherPeer;
13
13
  _bufferedAmount = 0;
14
+ destroyed = false;
14
15
  on(event, handler) {
15
16
  if (event === "data") {
16
17
  this.dataHandlers.push(handler);
@@ -41,6 +42,7 @@ class MockBinaryPeer {
41
42
  }
42
43
  }
43
44
  destroy() {
45
+ this.destroyed = true;
44
46
  for (const handler of this.closeHandlers) {
45
47
  handler();
46
48
  }
@@ -50,6 +52,20 @@ class MockBinaryPeer {
50
52
  getBufferedAmount() {
51
53
  return this._bufferedAmount;
52
54
  }
55
+ receive(data) {
56
+ for (const handler of this.dataHandlers) {
57
+ handler(data);
58
+ }
59
+ }
60
+ }
61
+ function rpcStartFrame(messageId, totalSize, chunk = new Uint8Array()) {
62
+ const frame = new Uint8Array(9 + chunk.length);
63
+ frame[0] = 0x01;
64
+ const view = new DataView(frame.buffer);
65
+ view.setUint32(1, messageId, false);
66
+ view.setUint32(5, totalSize, false);
67
+ frame.set(chunk, 9);
68
+ return frame;
53
69
  }
54
70
  async function getMockPeerPair() {
55
71
  const iPeer = new MockBinaryPeer();
@@ -270,6 +286,50 @@ describe("binary-peer-connection-v2", () => {
270
286
  rPeer.destroy();
271
287
  });
272
288
  });
289
+ describe("receive limits", () => {
290
+ it("closes before allocating an oversized declared RPC", async () => {
291
+ const { iPeer } = await getMockPeerPair();
292
+ (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("limited", iPeer, "wrtc", {
293
+ maxRpcMessageBytes: 32,
294
+ });
295
+ iPeer.receive(rpcStartFrame(1, 33));
296
+ expect(iPeer.destroyed).toBe(true);
297
+ });
298
+ it("bounds aggregate and concurrent incomplete reassemblies", async () => {
299
+ const { iPeer } = await getMockPeerPair();
300
+ (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("limited", iPeer, "wrtc", {
301
+ maxRpcMessageBytes: 32,
302
+ maxInFlightReassemblyBytes: 16,
303
+ maxConcurrentReassemblies: 2,
304
+ });
305
+ iPeer.receive(rpcStartFrame(1, 12, new Uint8Array([1])));
306
+ expect(iPeer.destroyed).toBe(false);
307
+ iPeer.receive(rpcStartFrame(2, 12, new Uint8Array([2])));
308
+ expect(iPeer.destroyed).toBe(true);
309
+ });
310
+ it("closes when an incomplete reassembly outlives its deadline", async () => {
311
+ const { iPeer } = await getMockPeerPair();
312
+ jest.useFakeTimers();
313
+ try {
314
+ (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("limited", iPeer, "wrtc", {
315
+ reassemblyTimeoutMs: 10,
316
+ });
317
+ iPeer.receive(rpcStartFrame(1, 32, new Uint8Array([1])));
318
+ jest.advanceTimersByTime(10);
319
+ expect(iPeer.destroyed).toBe(true);
320
+ }
321
+ finally {
322
+ jest.useRealTimers();
323
+ }
324
+ });
325
+ it("closes when continuation bytes exceed the declared RPC size", async () => {
326
+ const { iPeer } = await getMockPeerPair();
327
+ (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("limited", iPeer, "wrtc");
328
+ iPeer.receive(rpcStartFrame(1, 2, new Uint8Array([1])));
329
+ iPeer.receive(new Uint8Array([0x02, 0, 0, 0, 1, 2, 3]));
330
+ expect(iPeer.destroyed).toBe(true);
331
+ });
332
+ });
273
333
  describe("createBinaryPeerSocket raw bytes (stream mode)", () => {
274
334
  it("should send/receive raw bytes", async () => {
275
335
  const { iPeer, rPeer } = await getMockPeerPair();
@@ -27,12 +27,23 @@ export interface WrapBinaryPeerOptions {
27
27
  * When true, sets secureLocal and secureRemote to skip application-level encryption.
28
28
  */
29
29
  markTransportSecure?: boolean;
30
+ /** Optional hard limits for untrusted inbound messages. */
31
+ receiveLimits?: Partial<BinaryPeerReceiveLimits>;
32
+ }
33
+ /** Hard receive limits for the legacy binary peer parser. */
34
+ export interface BinaryPeerReceiveLimits {
35
+ /** Maximum bytes in one complete or reassembled RPC message. */
36
+ maxRpcMessageBytes: number;
37
+ /** Maximum bytes in one raw stream frame. */
38
+ maxStreamMessageBytes: number;
39
+ /** Maximum time an incomplete RPC message may retain memory. */
40
+ reassemblyTimeoutMs: number;
30
41
  }
31
42
  /**
32
43
  * Creates an ISocket from a binary peer without creating a full Connection.
33
44
  * Useful when you need to pass custom localServerAddresses to the Connection.
34
45
  */
35
- export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string): ISocket;
46
+ export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string, receiveLimitOverrides?: Partial<BinaryPeerReceiveLimits>): ISocket;
36
47
  /**
37
48
  * Wraps a binary peer (WebRTC or WebSocket) into a Connection.
38
49
  * This is the main implementation that handles all binary protocols.
@@ -43,5 +54,5 @@ export declare function wrapBinaryPeer(connectionId: string, peer: IBinaryPeer,
43
54
  * Backwards-compatible wrapper around wrapBinaryPeer.
44
55
  * WebRTC is encrypted at the transport layer (DTLS-SRTP), so we skip application-level encryption.
45
56
  */
46
- export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn): Connection;
57
+ export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn, receiveLimits?: Partial<BinaryPeerReceiveLimits>): Connection;
47
58
  export {};