@meshwhisper/sdk 0.1.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 (163) hide show
  1. package/README.md +138 -0
  2. package/dist/browser/index.d.ts +4 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +19 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/chaff/index.d.ts +91 -0
  7. package/dist/chaff/index.d.ts.map +1 -0
  8. package/dist/chaff/index.js +268 -0
  9. package/dist/chaff/index.js.map +1 -0
  10. package/dist/cluster/index.d.ts +159 -0
  11. package/dist/cluster/index.d.ts.map +1 -0
  12. package/dist/cluster/index.js +393 -0
  13. package/dist/cluster/index.js.map +1 -0
  14. package/dist/compliance/index.d.ts +129 -0
  15. package/dist/compliance/index.d.ts.map +1 -0
  16. package/dist/compliance/index.js +315 -0
  17. package/dist/compliance/index.js.map +1 -0
  18. package/dist/crypto/index.d.ts +65 -0
  19. package/dist/crypto/index.d.ts.map +1 -0
  20. package/dist/crypto/index.js +146 -0
  21. package/dist/crypto/index.js.map +1 -0
  22. package/dist/group/index.d.ts +155 -0
  23. package/dist/group/index.d.ts.map +1 -0
  24. package/dist/group/index.js +560 -0
  25. package/dist/group/index.js.map +1 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/namespace/index.d.ts +155 -0
  31. package/dist/namespace/index.d.ts.map +1 -0
  32. package/dist/namespace/index.js +278 -0
  33. package/dist/namespace/index.js.map +1 -0
  34. package/dist/node/index.d.ts +4 -0
  35. package/dist/node/index.d.ts.map +1 -0
  36. package/dist/node/index.js +19 -0
  37. package/dist/node/index.js.map +1 -0
  38. package/dist/packet/index.d.ts +63 -0
  39. package/dist/packet/index.d.ts.map +1 -0
  40. package/dist/packet/index.js +244 -0
  41. package/dist/packet/index.js.map +1 -0
  42. package/dist/permissions/index.d.ts +107 -0
  43. package/dist/permissions/index.d.ts.map +1 -0
  44. package/dist/permissions/index.js +282 -0
  45. package/dist/permissions/index.js.map +1 -0
  46. package/dist/persistence/idb-storage.d.ts +27 -0
  47. package/dist/persistence/idb-storage.d.ts.map +1 -0
  48. package/dist/persistence/idb-storage.js +75 -0
  49. package/dist/persistence/idb-storage.js.map +1 -0
  50. package/dist/persistence/index.d.ts +4 -0
  51. package/dist/persistence/index.d.ts.map +1 -0
  52. package/dist/persistence/index.js +3 -0
  53. package/dist/persistence/index.js.map +1 -0
  54. package/dist/persistence/node-storage.d.ts +33 -0
  55. package/dist/persistence/node-storage.d.ts.map +1 -0
  56. package/dist/persistence/node-storage.js +90 -0
  57. package/dist/persistence/node-storage.js.map +1 -0
  58. package/dist/persistence/serialization.d.ts +4 -0
  59. package/dist/persistence/serialization.d.ts.map +1 -0
  60. package/dist/persistence/serialization.js +49 -0
  61. package/dist/persistence/serialization.js.map +1 -0
  62. package/dist/persistence/types.d.ts +29 -0
  63. package/dist/persistence/types.d.ts.map +1 -0
  64. package/dist/persistence/types.js +5 -0
  65. package/dist/persistence/types.js.map +1 -0
  66. package/dist/ratchet/index.d.ts +80 -0
  67. package/dist/ratchet/index.d.ts.map +1 -0
  68. package/dist/ratchet/index.js +259 -0
  69. package/dist/ratchet/index.js.map +1 -0
  70. package/dist/reciprocity/index.d.ts +109 -0
  71. package/dist/reciprocity/index.d.ts.map +1 -0
  72. package/dist/reciprocity/index.js +311 -0
  73. package/dist/reciprocity/index.js.map +1 -0
  74. package/dist/relay/index.d.ts +87 -0
  75. package/dist/relay/index.d.ts.map +1 -0
  76. package/dist/relay/index.js +286 -0
  77. package/dist/relay/index.js.map +1 -0
  78. package/dist/routing/index.d.ts +136 -0
  79. package/dist/routing/index.d.ts.map +1 -0
  80. package/dist/routing/index.js +478 -0
  81. package/dist/routing/index.js.map +1 -0
  82. package/dist/sdk/index.d.ts +322 -0
  83. package/dist/sdk/index.d.ts.map +1 -0
  84. package/dist/sdk/index.js +1530 -0
  85. package/dist/sdk/index.js.map +1 -0
  86. package/dist/sybil/index.d.ts +123 -0
  87. package/dist/sybil/index.d.ts.map +1 -0
  88. package/dist/sybil/index.js +491 -0
  89. package/dist/sybil/index.js.map +1 -0
  90. package/dist/transport/browser/index.d.ts +34 -0
  91. package/dist/transport/browser/index.d.ts.map +1 -0
  92. package/dist/transport/browser/index.js +176 -0
  93. package/dist/transport/browser/index.js.map +1 -0
  94. package/dist/transport/local/index.d.ts +57 -0
  95. package/dist/transport/local/index.d.ts.map +1 -0
  96. package/dist/transport/local/index.js +442 -0
  97. package/dist/transport/local/index.js.map +1 -0
  98. package/dist/transport/negotiator/index.d.ts +79 -0
  99. package/dist/transport/negotiator/index.d.ts.map +1 -0
  100. package/dist/transport/negotiator/index.js +289 -0
  101. package/dist/transport/negotiator/index.js.map +1 -0
  102. package/dist/transport/node/index.d.ts +56 -0
  103. package/dist/transport/node/index.d.ts.map +1 -0
  104. package/dist/transport/node/index.js +209 -0
  105. package/dist/transport/node/index.js.map +1 -0
  106. package/dist/transport/noop/index.d.ts +11 -0
  107. package/dist/transport/noop/index.d.ts.map +1 -0
  108. package/dist/transport/noop/index.js +20 -0
  109. package/dist/transport/noop/index.js.map +1 -0
  110. package/dist/transport/p2p/index.d.ts +109 -0
  111. package/dist/transport/p2p/index.d.ts.map +1 -0
  112. package/dist/transport/p2p/index.js +237 -0
  113. package/dist/transport/p2p/index.js.map +1 -0
  114. package/dist/transport/websocket/index.d.ts +89 -0
  115. package/dist/transport/websocket/index.d.ts.map +1 -0
  116. package/dist/transport/websocket/index.js +498 -0
  117. package/dist/transport/websocket/index.js.map +1 -0
  118. package/dist/transport/websocket/serialize.d.ts +5 -0
  119. package/dist/transport/websocket/serialize.d.ts.map +1 -0
  120. package/dist/transport/websocket/serialize.js +55 -0
  121. package/dist/transport/websocket/serialize.js.map +1 -0
  122. package/dist/types.d.ts +215 -0
  123. package/dist/types.d.ts.map +1 -0
  124. package/dist/types.js +15 -0
  125. package/dist/types.js.map +1 -0
  126. package/dist/x3dh/index.d.ts +120 -0
  127. package/dist/x3dh/index.d.ts.map +1 -0
  128. package/dist/x3dh/index.js +290 -0
  129. package/dist/x3dh/index.js.map +1 -0
  130. package/package.json +59 -0
  131. package/src/browser/index.ts +19 -0
  132. package/src/chaff/index.ts +340 -0
  133. package/src/cluster/index.ts +482 -0
  134. package/src/compliance/index.ts +407 -0
  135. package/src/crypto/index.ts +193 -0
  136. package/src/group/index.ts +719 -0
  137. package/src/index.ts +87 -0
  138. package/src/lz4js.d.ts +58 -0
  139. package/src/namespace/index.ts +336 -0
  140. package/src/node/index.ts +19 -0
  141. package/src/packet/index.ts +326 -0
  142. package/src/permissions/index.ts +405 -0
  143. package/src/persistence/idb-storage.ts +83 -0
  144. package/src/persistence/index.ts +3 -0
  145. package/src/persistence/node-storage.ts +96 -0
  146. package/src/persistence/serialization.ts +75 -0
  147. package/src/persistence/types.ts +33 -0
  148. package/src/ratchet/index.ts +363 -0
  149. package/src/reciprocity/index.ts +371 -0
  150. package/src/relay/index.ts +382 -0
  151. package/src/routing/index.ts +577 -0
  152. package/src/sdk/index.ts +1994 -0
  153. package/src/sybil/index.ts +661 -0
  154. package/src/transport/browser/index.ts +201 -0
  155. package/src/transport/local/index.ts +540 -0
  156. package/src/transport/negotiator/index.ts +397 -0
  157. package/src/transport/node/index.ts +234 -0
  158. package/src/transport/noop/index.ts +22 -0
  159. package/src/transport/p2p/index.ts +345 -0
  160. package/src/transport/websocket/index.ts +660 -0
  161. package/src/transport/websocket/serialize.ts +68 -0
  162. package/src/types.ts +275 -0
  163. package/src/x3dh/index.ts +388 -0
@@ -0,0 +1,540 @@
1
+ // ============================================================
2
+ // MeshWhisper SDK — Local Network Transport (LAN)
3
+ // Bearer: local_net
4
+ //
5
+ // Uses UDP broadcast for peer discovery and TCP for reliable
6
+ // data transfer. Designed for device self-clustering on the
7
+ // same subnet (phone ↔ laptop in the same home).
8
+ // ============================================================
9
+
10
+ import * as dgram from 'node:dgram';
11
+ import * as net from 'node:net';
12
+ import type { Transport, Packet, PacketFlags } from '../../types.js';
13
+
14
+ // --- Constants ---
15
+
16
+ const MAGIC = 0x4d575350; // "MWSP"
17
+ const DEFAULT_UDP_PORT = 19205;
18
+ const DEFAULT_TCP_PORT = 19206;
19
+ const ANNOUNCE_INTERVAL_MS = 5_000;
20
+ const PEER_TTL_MS = 15_000;
21
+ const DEVICE_ID_LENGTH = 16;
22
+ const ANNOUNCEMENT_SIZE = 4 + DEVICE_ID_LENGTH + 2; // magic + id + port
23
+ const LENGTH_PREFIX_SIZE = 4; // uint32 big-endian frame header
24
+
25
+ // --- Discovered peer entry ---
26
+
27
+ interface DiscoveredPeer {
28
+ id: string;
29
+ address: string;
30
+ port: number;
31
+ lastSeen: number;
32
+ }
33
+
34
+ // --- TCP connection wrapper ---
35
+
36
+ interface PeerConnection {
37
+ peerId: string;
38
+ socket: net.Socket;
39
+ recvBuffer: Buffer;
40
+ }
41
+
42
+ // --- Helpers ---
43
+
44
+ /** Encode a 16-byte device ID to a hex string. */
45
+ function deviceIdToHex(buf: Uint8Array): string {
46
+ return Buffer.from(buf).toString('hex');
47
+ }
48
+
49
+ /** Decode a hex string back to a 16-byte Uint8Array. */
50
+ function hexToDeviceId(hex: string): Uint8Array {
51
+ return new Uint8Array(Buffer.from(hex, 'hex'));
52
+ }
53
+
54
+ /** Serialize a Packet to a binary buffer. */
55
+ function serializePacket(packet: Packet): Buffer {
56
+ const headerSize =
57
+ 1 + // version
58
+ 1 + // flags
59
+ 8 + // destHash
60
+ 16 + // senderEphemeralId
61
+ 1 + // ttl
62
+ 4; // payloadLength (uint32)
63
+ const buf = Buffer.alloc(headerSize + packet.encryptedPayload.length);
64
+ let offset = 0;
65
+
66
+ buf.writeUInt8(packet.version, offset);
67
+ offset += 1;
68
+ buf.writeUInt8(packet.flags, offset);
69
+ offset += 1;
70
+ Buffer.from(packet.destHash).copy(buf, offset, 0, 8);
71
+ offset += 8;
72
+ Buffer.from(packet.senderEphemeralId).copy(buf, offset, 0, 16);
73
+ offset += 16;
74
+ buf.writeUInt8(packet.ttl, offset);
75
+ offset += 1;
76
+ buf.writeUInt32BE(packet.encryptedPayload.length, offset);
77
+ offset += 4;
78
+ Buffer.from(packet.encryptedPayload).copy(buf, offset);
79
+
80
+ return buf;
81
+ }
82
+
83
+ /** Deserialize a binary buffer back into a Packet. */
84
+ function deserializePacket(buf: Buffer): Packet {
85
+ let offset = 0;
86
+
87
+ const version = buf.readUInt8(offset);
88
+ offset += 1;
89
+ const flags = buf.readUInt8(offset) as PacketFlags;
90
+ offset += 1;
91
+ const destHash = new Uint8Array(buf.subarray(offset, offset + 8));
92
+ offset += 8;
93
+ const senderEphemeralId = new Uint8Array(buf.subarray(offset, offset + 16));
94
+ offset += 16;
95
+ const ttl = buf.readUInt8(offset);
96
+ offset += 1;
97
+ const payloadLength = buf.readUInt32BE(offset);
98
+ offset += 4;
99
+ const encryptedPayload = new Uint8Array(buf.subarray(offset, offset + payloadLength));
100
+
101
+ return { version, flags, destHash, senderEphemeralId, ttl, payloadLength, encryptedPayload };
102
+ }
103
+
104
+ // ============================================================
105
+ // LocalTransport
106
+ // ============================================================
107
+
108
+ export class LocalTransport implements Transport {
109
+ readonly type = 'local_net' as const;
110
+
111
+ // --- Configuration ---
112
+ private readonly deviceId: Uint8Array;
113
+ private readonly deviceIdHex: string;
114
+ private readonly udpPort: number;
115
+ private readonly tcpPort: number;
116
+
117
+ // --- Networking ---
118
+ private udpSocket: dgram.Socket | null = null;
119
+ private tcpServer: net.Server | null = null;
120
+ private announceTimer: ReturnType<typeof setInterval> | null = null;
121
+ private pruneTimer: ReturnType<typeof setInterval> | null = null;
122
+
123
+ // --- State ---
124
+ private readonly discoveredPeers = new Map<string, DiscoveredPeer>();
125
+ private readonly connections = new Map<string, PeerConnection>();
126
+ private readonly pendingConnections = new Set<string>(); // addresses currently being connected to
127
+ private receiveCallback: ((packet: Packet, source: string) => void) | null = null;
128
+ private running = false;
129
+
130
+ constructor(
131
+ deviceId: Uint8Array,
132
+ options?: { udpPort?: number; tcpPort?: number },
133
+ ) {
134
+ if (deviceId.length !== DEVICE_ID_LENGTH) {
135
+ throw new Error(`deviceId must be ${DEVICE_ID_LENGTH} bytes, got ${deviceId.length}`);
136
+ }
137
+ this.deviceId = deviceId;
138
+ this.deviceIdHex = deviceIdToHex(deviceId);
139
+ this.udpPort = options?.udpPort ?? DEFAULT_UDP_PORT;
140
+ this.tcpPort = options?.tcpPort ?? DEFAULT_TCP_PORT;
141
+ }
142
+
143
+ // --------------------------------------------------------
144
+ // Transport interface — lifecycle
145
+ // --------------------------------------------------------
146
+
147
+ async start(): Promise<void> {
148
+ if (this.running) return;
149
+ this.running = true;
150
+
151
+ await Promise.all([
152
+ this.startDiscovery(),
153
+ this.startListener(this.tcpPort),
154
+ ]);
155
+ }
156
+
157
+ async stop(): Promise<void> {
158
+ if (!this.running) return;
159
+ this.running = false;
160
+
161
+ // Clear timers
162
+ if (this.announceTimer) {
163
+ clearInterval(this.announceTimer);
164
+ this.announceTimer = null;
165
+ }
166
+ if (this.pruneTimer) {
167
+ clearInterval(this.pruneTimer);
168
+ this.pruneTimer = null;
169
+ }
170
+
171
+ // Close all TCP peer connections
172
+ for (const [, conn] of this.connections) {
173
+ conn.socket.destroy();
174
+ }
175
+ this.connections.clear();
176
+ this.pendingConnections.clear();
177
+
178
+ // Close TCP server
179
+ await new Promise<void>((resolve) => {
180
+ if (this.tcpServer) {
181
+ this.tcpServer.close(() => resolve());
182
+ } else {
183
+ resolve();
184
+ }
185
+ });
186
+ this.tcpServer = null;
187
+
188
+ // Close UDP socket
189
+ await new Promise<void>((resolve) => {
190
+ if (this.udpSocket) {
191
+ this.udpSocket.close(() => resolve());
192
+ } else {
193
+ resolve();
194
+ }
195
+ });
196
+ this.udpSocket = null;
197
+
198
+ this.discoveredPeers.clear();
199
+ }
200
+
201
+ async isAvailable(): Promise<boolean> {
202
+ // Local network is available if we can bind a UDP socket.
203
+ // In practice this checks whether the OS networking stack is usable.
204
+ return new Promise<boolean>((resolve) => {
205
+ const probe = dgram.createSocket({ type: 'udp4', reuseAddr: true });
206
+ probe.on('error', () => {
207
+ probe.close();
208
+ resolve(false);
209
+ });
210
+ probe.bind(0, () => {
211
+ probe.close();
212
+ resolve(true);
213
+ });
214
+ });
215
+ }
216
+
217
+ // --------------------------------------------------------
218
+ // Transport interface — messaging
219
+ // --------------------------------------------------------
220
+
221
+ async send(packet: Packet, destination: string): Promise<void> {
222
+ const conn = this.connections.get(destination);
223
+ if (!conn) {
224
+ throw new Error(`No active connection to peer ${destination}`);
225
+ }
226
+
227
+ const payload = serializePacket(packet);
228
+ const frame = Buffer.alloc(LENGTH_PREFIX_SIZE + payload.length);
229
+ frame.writeUInt32BE(payload.length, 0);
230
+ payload.copy(frame, LENGTH_PREFIX_SIZE);
231
+
232
+ await new Promise<void>((resolve, reject) => {
233
+ conn.socket.write(frame, (err) => {
234
+ if (err) reject(err);
235
+ else resolve();
236
+ });
237
+ });
238
+ }
239
+
240
+ onReceive(callback: (packet: Packet, source: string) => void): void {
241
+ this.receiveCallback = callback;
242
+ }
243
+
244
+ // --------------------------------------------------------
245
+ // UDP Discovery
246
+ // --------------------------------------------------------
247
+
248
+ async startDiscovery(): Promise<void> {
249
+ await new Promise<void>((resolve, reject) => {
250
+ this.udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
251
+
252
+ this.udpSocket.on('error', (err) => {
253
+ if (!this.running) return;
254
+ // Non-fatal in steady state; during bind it rejects the promise.
255
+ reject(err);
256
+ });
257
+
258
+ this.udpSocket.on('message', (msg, rinfo) => {
259
+ this.handleAnnouncement(msg, rinfo.address);
260
+ });
261
+
262
+ this.udpSocket.bind(this.udpPort, () => {
263
+ this.udpSocket!.setBroadcast(true);
264
+
265
+ // Send first announcement immediately, then on interval
266
+ this.broadcastAnnouncement();
267
+ this.announceTimer = setInterval(
268
+ () => this.broadcastAnnouncement(),
269
+ ANNOUNCE_INTERVAL_MS,
270
+ );
271
+
272
+ // Periodically prune stale peers
273
+ this.pruneTimer = setInterval(
274
+ () => this.pruneStalePeers(),
275
+ ANNOUNCE_INTERVAL_MS,
276
+ );
277
+
278
+ resolve();
279
+ });
280
+ });
281
+ }
282
+
283
+ /** Build and broadcast a MWSP announcement datagram. */
284
+ private broadcastAnnouncement(): void {
285
+ if (!this.udpSocket) return;
286
+
287
+ const buf = Buffer.alloc(ANNOUNCEMENT_SIZE);
288
+ let offset = 0;
289
+
290
+ buf.writeUInt32BE(MAGIC, offset);
291
+ offset += 4;
292
+ Buffer.from(this.deviceId).copy(buf, offset, 0, DEVICE_ID_LENGTH);
293
+ offset += DEVICE_ID_LENGTH;
294
+ buf.writeUInt16BE(this.tcpPort, offset);
295
+
296
+ this.udpSocket.send(buf, 0, buf.length, this.udpPort, '255.255.255.255', (err) => {
297
+ if (err && this.running) {
298
+ // Best-effort; swallow transient send errors.
299
+ }
300
+ });
301
+ }
302
+
303
+ /** Process an incoming UDP announcement. */
304
+ private handleAnnouncement(msg: Buffer, senderAddress: string): void {
305
+ if (msg.length < ANNOUNCEMENT_SIZE) return;
306
+
307
+ const magic = msg.readUInt32BE(0);
308
+ if (magic !== MAGIC) return;
309
+
310
+ const peerIdBytes = msg.subarray(4, 4 + DEVICE_ID_LENGTH);
311
+ const peerId = deviceIdToHex(peerIdBytes);
312
+
313
+ // Ignore our own announcements
314
+ if (peerId === this.deviceIdHex) return;
315
+
316
+ const tcpPort = msg.readUInt16BE(4 + DEVICE_ID_LENGTH);
317
+
318
+ const existing = this.discoveredPeers.get(peerId);
319
+ if (existing) {
320
+ existing.lastSeen = Date.now();
321
+ existing.address = senderAddress;
322
+ existing.port = tcpPort;
323
+ } else {
324
+ this.discoveredPeers.set(peerId, {
325
+ id: peerId,
326
+ address: senderAddress,
327
+ port: tcpPort,
328
+ lastSeen: Date.now(),
329
+ });
330
+ }
331
+
332
+ // Auto-connect if we don't already have a TCP connection
333
+ if (!this.connections.has(peerId) && !this.pendingConnections.has(peerId)) {
334
+ this.connectToPeer(senderAddress, tcpPort).catch(() => {
335
+ // Connection failed; will retry on next announcement.
336
+ });
337
+ }
338
+ }
339
+
340
+ /** Remove peers whose last announcement is older than PEER_TTL_MS. */
341
+ private pruneStalePeers(): void {
342
+ const now = Date.now();
343
+ for (const [id, peer] of this.discoveredPeers) {
344
+ if (now - peer.lastSeen > PEER_TTL_MS) {
345
+ this.discoveredPeers.delete(id);
346
+ // Also tear down stale TCP connections
347
+ const conn = this.connections.get(id);
348
+ if (conn) {
349
+ conn.socket.destroy();
350
+ this.connections.delete(id);
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ // --------------------------------------------------------
357
+ // TCP Data Channel
358
+ // --------------------------------------------------------
359
+
360
+ async startListener(port?: number): Promise<void> {
361
+ const listenPort = port ?? this.tcpPort;
362
+
363
+ await new Promise<void>((resolve, reject) => {
364
+ this.tcpServer = net.createServer((socket) => {
365
+ this.handleIncomingConnection(socket);
366
+ });
367
+
368
+ this.tcpServer.on('error', (err) => {
369
+ reject(err);
370
+ });
371
+
372
+ this.tcpServer.listen(listenPort, () => {
373
+ resolve();
374
+ });
375
+ });
376
+ }
377
+
378
+ async connectToPeer(address: string, port: number): Promise<void> {
379
+ // Derive a temporary key until the peer identifies itself via handshake.
380
+ const addrKey = `${address}:${port}`;
381
+ this.pendingConnections.add(addrKey);
382
+
383
+ return new Promise<void>((resolve, reject) => {
384
+ const socket = net.createConnection({ host: address, port }, () => {
385
+ // Send our device ID so the remote side knows who connected
386
+ const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
387
+ idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
388
+ Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
389
+ socket.write(idFrame);
390
+
391
+ // We don't yet know the peer ID. We'll register the connection
392
+ // once we receive the peer's ID frame back.
393
+ const conn: PeerConnection = {
394
+ peerId: '', // will be populated
395
+ socket,
396
+ recvBuffer: Buffer.alloc(0),
397
+ };
398
+ this.setupTcpFraming(conn, true);
399
+ this.pendingConnections.delete(addrKey);
400
+ resolve();
401
+ });
402
+
403
+ socket.on('error', (err) => {
404
+ this.pendingConnections.delete(addrKey);
405
+ reject(err);
406
+ });
407
+ });
408
+ }
409
+
410
+ /** Handle an incoming TCP connection from a remote peer. */
411
+ private handleIncomingConnection(socket: net.Socket): void {
412
+ const conn: PeerConnection = {
413
+ peerId: '', // unknown until the peer sends its ID frame
414
+ socket,
415
+ recvBuffer: Buffer.alloc(0),
416
+ };
417
+
418
+ // The first framed message from the connecting side is the device ID.
419
+ this.setupTcpFraming(conn, false);
420
+
421
+ // Send our own ID back so the remote side can register us.
422
+ const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
423
+ idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
424
+ Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
425
+ socket.write(idFrame);
426
+ }
427
+
428
+ /**
429
+ * Attach length-prefixed framing to a TCP connection.
430
+ *
431
+ * The first message on every connection is a 16-byte device ID used to
432
+ * register the peer. All subsequent messages are serialized Packets.
433
+ *
434
+ * @param conn The peer connection wrapper (mutated in place).
435
+ * @param isInitiator True if we initiated the connection.
436
+ */
437
+ private setupTcpFraming(conn: PeerConnection, isInitiator: boolean): void {
438
+ let identified = false;
439
+
440
+ conn.socket.on('data', (chunk: Buffer) => {
441
+ conn.recvBuffer = Buffer.concat([conn.recvBuffer, chunk]);
442
+
443
+ // Process as many complete frames as available
444
+ while (conn.recvBuffer.length >= LENGTH_PREFIX_SIZE) {
445
+ const frameLen = conn.recvBuffer.readUInt32BE(0);
446
+
447
+ // Guard against absurdly large frames (16 MiB limit)
448
+ if (frameLen > 16 * 1024 * 1024) {
449
+ conn.socket.destroy(new Error('Frame too large'));
450
+ return;
451
+ }
452
+
453
+ if (conn.recvBuffer.length < LENGTH_PREFIX_SIZE + frameLen) {
454
+ break; // wait for more data
455
+ }
456
+
457
+ const frameData = conn.recvBuffer.subarray(
458
+ LENGTH_PREFIX_SIZE,
459
+ LENGTH_PREFIX_SIZE + frameLen,
460
+ );
461
+ conn.recvBuffer = Buffer.from(
462
+ conn.recvBuffer.subarray(LENGTH_PREFIX_SIZE + frameLen),
463
+ );
464
+
465
+ if (!identified) {
466
+ // First frame: device ID
467
+ if (frameData.length !== DEVICE_ID_LENGTH) {
468
+ conn.socket.destroy(new Error('Invalid identification frame'));
469
+ return;
470
+ }
471
+ const peerId = deviceIdToHex(frameData);
472
+
473
+ // Don't connect to ourselves
474
+ if (peerId === this.deviceIdHex) {
475
+ conn.socket.destroy();
476
+ return;
477
+ }
478
+
479
+ // If we already have a connection to this peer, keep only one.
480
+ // The tie-breaker: the side with the lexicographically smaller ID
481
+ // keeps its *initiated* connection.
482
+ const existingConn = this.connections.get(peerId);
483
+ if (existingConn) {
484
+ const weAreSmaller = this.deviceIdHex < peerId;
485
+ if (isInitiator === weAreSmaller) {
486
+ // We keep this connection; destroy the old one.
487
+ existingConn.socket.destroy();
488
+ } else {
489
+ // We keep the existing connection; destroy this one.
490
+ conn.socket.destroy();
491
+ return;
492
+ }
493
+ }
494
+
495
+ conn.peerId = peerId;
496
+ this.connections.set(peerId, conn);
497
+ identified = true;
498
+ } else {
499
+ // Subsequent frames: Packets
500
+ try {
501
+ const packet = deserializePacket(Buffer.from(frameData));
502
+ this.receiveCallback?.(packet, conn.peerId);
503
+ } catch {
504
+ // Malformed packet — drop silently.
505
+ }
506
+ }
507
+ }
508
+ });
509
+
510
+ conn.socket.on('close', () => {
511
+ if (conn.peerId && this.connections.get(conn.peerId) === conn) {
512
+ this.connections.delete(conn.peerId);
513
+ }
514
+ });
515
+
516
+ conn.socket.on('error', () => {
517
+ // Error is followed by close; cleanup happens there.
518
+ });
519
+ }
520
+
521
+ // --------------------------------------------------------
522
+ // Peer Queries
523
+ // --------------------------------------------------------
524
+
525
+ /** Return the list of peers discovered via UDP announcements. */
526
+ getDiscoveredPeers(): Array<{ id: string; address: string; port: number }> {
527
+ return Array.from(this.discoveredPeers.values()).map(({ id, address, port }) => ({
528
+ id,
529
+ address,
530
+ port,
531
+ }));
532
+ }
533
+
534
+ /** Return the IDs of peers with an active TCP connection. */
535
+ getConnectedPeers(): string[] {
536
+ return Array.from(this.connections.keys());
537
+ }
538
+ }
539
+
540
+ export default LocalTransport;