@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,176 @@
1
+ // ============================================================
2
+ // MeshWhisper SDK — Browser Transport
3
+ // Connects the SDK to a MeshWhisper Node using the native
4
+ // browser WebSocket API. Drop-in replacement for NodeTransport
5
+ // when running in a browser or PWA context.
6
+ // ============================================================
7
+ import { serializePacket, deserializePacket, HEADER_SIZE, } from '../websocket/serialize.js';
8
+ // ---- Constants ----
9
+ /** Foundation-hosted relay nodes. Used when node config is "mesh". */
10
+ export const FOUNDATION_RELAY_NODES = [
11
+ 'wss://relay.meshwhisper.io',
12
+ ];
13
+ const RECONNECT_BASE_MS = 1_000;
14
+ const RECONNECT_MAX_MS = 30_000;
15
+ // ---- BrowserTransport ----
16
+ /**
17
+ * Transport that connects to a MeshWhisper Node using the native browser
18
+ * WebSocket API. Functionally identical to NodeTransport but uses
19
+ * `globalThis.WebSocket` instead of the `ws` npm package, so it bundles
20
+ * cleanly for browsers and PWAs.
21
+ */
22
+ export class BrowserTransport {
23
+ nodeUrl;
24
+ getDestHashes;
25
+ pushConfig;
26
+ type = 'internet';
27
+ ws = null;
28
+ receiveCallbacks = [];
29
+ running = false;
30
+ reconnectAttempt = 0;
31
+ reconnectTimer = null;
32
+ constructor(nodeUrl, getDestHashes, pushConfig) {
33
+ this.nodeUrl = nodeUrl;
34
+ this.getDestHashes = getDestHashes;
35
+ this.pushConfig = pushConfig;
36
+ }
37
+ // ---- Transport interface ----
38
+ async start() {
39
+ this.running = true;
40
+ await this.connect();
41
+ }
42
+ async stop() {
43
+ this.running = false;
44
+ if (this.reconnectTimer) {
45
+ clearTimeout(this.reconnectTimer);
46
+ this.reconnectTimer = null;
47
+ }
48
+ if (this.ws) {
49
+ this.ws.close(1000, 'shutdown');
50
+ this.ws = null;
51
+ }
52
+ }
53
+ async isAvailable() {
54
+ return this.ws?.readyState === WebSocket.OPEN;
55
+ }
56
+ async send(packet, _destination) {
57
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
58
+ throw new Error('BrowserTransport: not connected to Node');
59
+ }
60
+ const binary = serializePacket(packet);
61
+ this.ws.send(binary);
62
+ }
63
+ onReceive(callback) {
64
+ this.receiveCallbacks.push(callback);
65
+ }
66
+ // ---- Connection management ----
67
+ resolveUrl() {
68
+ if (this.nodeUrl === 'mesh') {
69
+ return FOUNDATION_RELAY_NODES[0];
70
+ }
71
+ return this.nodeUrl;
72
+ }
73
+ connect() {
74
+ const url = this.resolveUrl();
75
+ return new Promise((resolve, reject) => {
76
+ let resolved = false;
77
+ const ws = new WebSocket(url);
78
+ ws.binaryType = 'arraybuffer';
79
+ ws.addEventListener('open', () => {
80
+ this.ws = ws;
81
+ this.reconnectAttempt = 0;
82
+ resolved = true;
83
+ ws.send(JSON.stringify(this.buildHello()));
84
+ resolve();
85
+ });
86
+ ws.addEventListener('message', (event) => {
87
+ this.handleMessage(event.data);
88
+ });
89
+ ws.addEventListener('close', () => {
90
+ this.ws = null;
91
+ if (this.running)
92
+ this.scheduleReconnect();
93
+ });
94
+ ws.addEventListener('error', () => {
95
+ if (!resolved) {
96
+ resolved = true;
97
+ reject(new Error(`BrowserTransport: failed to connect to ${url}`));
98
+ }
99
+ });
100
+ });
101
+ }
102
+ scheduleReconnect() {
103
+ if (!this.running)
104
+ return;
105
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), RECONNECT_MAX_MS);
106
+ this.reconnectAttempt++;
107
+ this.reconnectTimer = setTimeout(async () => {
108
+ if (!this.running)
109
+ return;
110
+ try {
111
+ await this.connect();
112
+ }
113
+ catch {
114
+ this.scheduleReconnect();
115
+ }
116
+ }, delay);
117
+ }
118
+ // ---- Message handling ----
119
+ handleMessage(data) {
120
+ if (data instanceof ArrayBuffer && data.byteLength >= HEADER_SIZE) {
121
+ try {
122
+ const packet = deserializePacket(new Uint8Array(data));
123
+ const source = this.resolveUrl();
124
+ for (const cb of this.receiveCallbacks) {
125
+ try {
126
+ cb(packet, source);
127
+ }
128
+ catch { /* swallow */ }
129
+ }
130
+ }
131
+ catch {
132
+ // Malformed packet — discard
133
+ }
134
+ return;
135
+ }
136
+ if (typeof data === 'string') {
137
+ try {
138
+ JSON.parse(data);
139
+ // Reserved for future Node→client control messages
140
+ }
141
+ catch { /* not JSON */ }
142
+ }
143
+ }
144
+ // ---- Dest hash / push token refresh ----
145
+ refreshDestHashes() {
146
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
147
+ return;
148
+ this.ws.send(JSON.stringify(this.buildHello()));
149
+ }
150
+ setPushConfig(pushConfig) {
151
+ this.pushConfig = pushConfig;
152
+ this.refreshDestHashes();
153
+ }
154
+ // ---- Private helpers ----
155
+ buildHello() {
156
+ const msg = {
157
+ type: 'hello',
158
+ destHashes: this.getDestHashes(),
159
+ };
160
+ if (this.pushConfig) {
161
+ if (this.pushConfig.platform === 'webpush') {
162
+ msg['pushSubscription'] = JSON.stringify(this.pushConfig.subscription);
163
+ msg['pushPlatform'] = 'webpush';
164
+ }
165
+ else {
166
+ msg['pushToken'] = this.pushConfig.token;
167
+ msg['pushPlatform'] = this.pushConfig.platform;
168
+ if ('topic' in this.pushConfig && this.pushConfig.topic) {
169
+ msg['pushTopic'] = this.pushConfig.topic;
170
+ }
171
+ }
172
+ }
173
+ return msg;
174
+ }
175
+ }
176
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/browser/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { Transport, Packet } from '../../types.js';
2
+ export declare class LocalTransport implements Transport {
3
+ readonly type: "local_net";
4
+ private readonly deviceId;
5
+ private readonly deviceIdHex;
6
+ private readonly udpPort;
7
+ private readonly tcpPort;
8
+ private udpSocket;
9
+ private tcpServer;
10
+ private announceTimer;
11
+ private pruneTimer;
12
+ private readonly discoveredPeers;
13
+ private readonly connections;
14
+ private readonly pendingConnections;
15
+ private receiveCallback;
16
+ private running;
17
+ constructor(deviceId: Uint8Array, options?: {
18
+ udpPort?: number;
19
+ tcpPort?: number;
20
+ });
21
+ start(): Promise<void>;
22
+ stop(): Promise<void>;
23
+ isAvailable(): Promise<boolean>;
24
+ send(packet: Packet, destination: string): Promise<void>;
25
+ onReceive(callback: (packet: Packet, source: string) => void): void;
26
+ startDiscovery(): Promise<void>;
27
+ /** Build and broadcast a MWSP announcement datagram. */
28
+ private broadcastAnnouncement;
29
+ /** Process an incoming UDP announcement. */
30
+ private handleAnnouncement;
31
+ /** Remove peers whose last announcement is older than PEER_TTL_MS. */
32
+ private pruneStalePeers;
33
+ startListener(port?: number): Promise<void>;
34
+ connectToPeer(address: string, port: number): Promise<void>;
35
+ /** Handle an incoming TCP connection from a remote peer. */
36
+ private handleIncomingConnection;
37
+ /**
38
+ * Attach length-prefixed framing to a TCP connection.
39
+ *
40
+ * The first message on every connection is a 16-byte device ID used to
41
+ * register the peer. All subsequent messages are serialized Packets.
42
+ *
43
+ * @param conn The peer connection wrapper (mutated in place).
44
+ * @param isInitiator True if we initiated the connection.
45
+ */
46
+ private setupTcpFraming;
47
+ /** Return the list of peers discovered via UDP announcements. */
48
+ getDiscoveredPeers(): Array<{
49
+ id: string;
50
+ address: string;
51
+ port: number;
52
+ }>;
53
+ /** Return the IDs of peers with an active TCP connection. */
54
+ getConnectedPeers(): string[];
55
+ }
56
+ export default LocalTransport;
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transport/local/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAe,MAAM,gBAAgB,CAAC;AAgGrE,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IAGrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAGjC,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,eAAe,CAA2D;IAClF,OAAO,CAAC,OAAO,CAAS;gBAGtB,QAAQ,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAe5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CrB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAoB/B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9D,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAQ7D,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCrC,wDAAwD;IACxD,OAAO,CAAC,qBAAqB;IAmB7B,4CAA4C;IAC5C,OAAO,CAAC,kBAAkB;IAoC1B,sEAAsE;IACtE,OAAO,CAAC,eAAe;IAmBjB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB3C,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCjE,4DAA4D;IAC5D,OAAO,CAAC,wBAAwB;IAiBhC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IAwFvB,iEAAiE;IACjE,kBAAkB,IAAI,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAQ1E,6DAA6D;IAC7D,iBAAiB,IAAI,MAAM,EAAE;CAG9B;AAED,eAAe,cAAc,CAAC"}
@@ -0,0 +1,442 @@
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
+ import * as dgram from 'node:dgram';
10
+ import * as net from 'node:net';
11
+ // --- Constants ---
12
+ const MAGIC = 0x4d575350; // "MWSP"
13
+ const DEFAULT_UDP_PORT = 19205;
14
+ const DEFAULT_TCP_PORT = 19206;
15
+ const ANNOUNCE_INTERVAL_MS = 5_000;
16
+ const PEER_TTL_MS = 15_000;
17
+ const DEVICE_ID_LENGTH = 16;
18
+ const ANNOUNCEMENT_SIZE = 4 + DEVICE_ID_LENGTH + 2; // magic + id + port
19
+ const LENGTH_PREFIX_SIZE = 4; // uint32 big-endian frame header
20
+ // --- Helpers ---
21
+ /** Encode a 16-byte device ID to a hex string. */
22
+ function deviceIdToHex(buf) {
23
+ return Buffer.from(buf).toString('hex');
24
+ }
25
+ /** Decode a hex string back to a 16-byte Uint8Array. */
26
+ function hexToDeviceId(hex) {
27
+ return new Uint8Array(Buffer.from(hex, 'hex'));
28
+ }
29
+ /** Serialize a Packet to a binary buffer. */
30
+ function serializePacket(packet) {
31
+ const headerSize = 1 + // version
32
+ 1 + // flags
33
+ 8 + // destHash
34
+ 16 + // senderEphemeralId
35
+ 1 + // ttl
36
+ 4; // payloadLength (uint32)
37
+ const buf = Buffer.alloc(headerSize + packet.encryptedPayload.length);
38
+ let offset = 0;
39
+ buf.writeUInt8(packet.version, offset);
40
+ offset += 1;
41
+ buf.writeUInt8(packet.flags, offset);
42
+ offset += 1;
43
+ Buffer.from(packet.destHash).copy(buf, offset, 0, 8);
44
+ offset += 8;
45
+ Buffer.from(packet.senderEphemeralId).copy(buf, offset, 0, 16);
46
+ offset += 16;
47
+ buf.writeUInt8(packet.ttl, offset);
48
+ offset += 1;
49
+ buf.writeUInt32BE(packet.encryptedPayload.length, offset);
50
+ offset += 4;
51
+ Buffer.from(packet.encryptedPayload).copy(buf, offset);
52
+ return buf;
53
+ }
54
+ /** Deserialize a binary buffer back into a Packet. */
55
+ function deserializePacket(buf) {
56
+ let offset = 0;
57
+ const version = buf.readUInt8(offset);
58
+ offset += 1;
59
+ const flags = buf.readUInt8(offset);
60
+ offset += 1;
61
+ const destHash = new Uint8Array(buf.subarray(offset, offset + 8));
62
+ offset += 8;
63
+ const senderEphemeralId = new Uint8Array(buf.subarray(offset, offset + 16));
64
+ offset += 16;
65
+ const ttl = buf.readUInt8(offset);
66
+ offset += 1;
67
+ const payloadLength = buf.readUInt32BE(offset);
68
+ offset += 4;
69
+ const encryptedPayload = new Uint8Array(buf.subarray(offset, offset + payloadLength));
70
+ return { version, flags, destHash, senderEphemeralId, ttl, payloadLength, encryptedPayload };
71
+ }
72
+ // ============================================================
73
+ // LocalTransport
74
+ // ============================================================
75
+ export class LocalTransport {
76
+ type = 'local_net';
77
+ // --- Configuration ---
78
+ deviceId;
79
+ deviceIdHex;
80
+ udpPort;
81
+ tcpPort;
82
+ // --- Networking ---
83
+ udpSocket = null;
84
+ tcpServer = null;
85
+ announceTimer = null;
86
+ pruneTimer = null;
87
+ // --- State ---
88
+ discoveredPeers = new Map();
89
+ connections = new Map();
90
+ pendingConnections = new Set(); // addresses currently being connected to
91
+ receiveCallback = null;
92
+ running = false;
93
+ constructor(deviceId, options) {
94
+ if (deviceId.length !== DEVICE_ID_LENGTH) {
95
+ throw new Error(`deviceId must be ${DEVICE_ID_LENGTH} bytes, got ${deviceId.length}`);
96
+ }
97
+ this.deviceId = deviceId;
98
+ this.deviceIdHex = deviceIdToHex(deviceId);
99
+ this.udpPort = options?.udpPort ?? DEFAULT_UDP_PORT;
100
+ this.tcpPort = options?.tcpPort ?? DEFAULT_TCP_PORT;
101
+ }
102
+ // --------------------------------------------------------
103
+ // Transport interface — lifecycle
104
+ // --------------------------------------------------------
105
+ async start() {
106
+ if (this.running)
107
+ return;
108
+ this.running = true;
109
+ await Promise.all([
110
+ this.startDiscovery(),
111
+ this.startListener(this.tcpPort),
112
+ ]);
113
+ }
114
+ async stop() {
115
+ if (!this.running)
116
+ return;
117
+ this.running = false;
118
+ // Clear timers
119
+ if (this.announceTimer) {
120
+ clearInterval(this.announceTimer);
121
+ this.announceTimer = null;
122
+ }
123
+ if (this.pruneTimer) {
124
+ clearInterval(this.pruneTimer);
125
+ this.pruneTimer = null;
126
+ }
127
+ // Close all TCP peer connections
128
+ for (const [, conn] of this.connections) {
129
+ conn.socket.destroy();
130
+ }
131
+ this.connections.clear();
132
+ this.pendingConnections.clear();
133
+ // Close TCP server
134
+ await new Promise((resolve) => {
135
+ if (this.tcpServer) {
136
+ this.tcpServer.close(() => resolve());
137
+ }
138
+ else {
139
+ resolve();
140
+ }
141
+ });
142
+ this.tcpServer = null;
143
+ // Close UDP socket
144
+ await new Promise((resolve) => {
145
+ if (this.udpSocket) {
146
+ this.udpSocket.close(() => resolve());
147
+ }
148
+ else {
149
+ resolve();
150
+ }
151
+ });
152
+ this.udpSocket = null;
153
+ this.discoveredPeers.clear();
154
+ }
155
+ async isAvailable() {
156
+ // Local network is available if we can bind a UDP socket.
157
+ // In practice this checks whether the OS networking stack is usable.
158
+ return new Promise((resolve) => {
159
+ const probe = dgram.createSocket({ type: 'udp4', reuseAddr: true });
160
+ probe.on('error', () => {
161
+ probe.close();
162
+ resolve(false);
163
+ });
164
+ probe.bind(0, () => {
165
+ probe.close();
166
+ resolve(true);
167
+ });
168
+ });
169
+ }
170
+ // --------------------------------------------------------
171
+ // Transport interface — messaging
172
+ // --------------------------------------------------------
173
+ async send(packet, destination) {
174
+ const conn = this.connections.get(destination);
175
+ if (!conn) {
176
+ throw new Error(`No active connection to peer ${destination}`);
177
+ }
178
+ const payload = serializePacket(packet);
179
+ const frame = Buffer.alloc(LENGTH_PREFIX_SIZE + payload.length);
180
+ frame.writeUInt32BE(payload.length, 0);
181
+ payload.copy(frame, LENGTH_PREFIX_SIZE);
182
+ await new Promise((resolve, reject) => {
183
+ conn.socket.write(frame, (err) => {
184
+ if (err)
185
+ reject(err);
186
+ else
187
+ resolve();
188
+ });
189
+ });
190
+ }
191
+ onReceive(callback) {
192
+ this.receiveCallback = callback;
193
+ }
194
+ // --------------------------------------------------------
195
+ // UDP Discovery
196
+ // --------------------------------------------------------
197
+ async startDiscovery() {
198
+ await new Promise((resolve, reject) => {
199
+ this.udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
200
+ this.udpSocket.on('error', (err) => {
201
+ if (!this.running)
202
+ return;
203
+ // Non-fatal in steady state; during bind it rejects the promise.
204
+ reject(err);
205
+ });
206
+ this.udpSocket.on('message', (msg, rinfo) => {
207
+ this.handleAnnouncement(msg, rinfo.address);
208
+ });
209
+ this.udpSocket.bind(this.udpPort, () => {
210
+ this.udpSocket.setBroadcast(true);
211
+ // Send first announcement immediately, then on interval
212
+ this.broadcastAnnouncement();
213
+ this.announceTimer = setInterval(() => this.broadcastAnnouncement(), ANNOUNCE_INTERVAL_MS);
214
+ // Periodically prune stale peers
215
+ this.pruneTimer = setInterval(() => this.pruneStalePeers(), ANNOUNCE_INTERVAL_MS);
216
+ resolve();
217
+ });
218
+ });
219
+ }
220
+ /** Build and broadcast a MWSP announcement datagram. */
221
+ broadcastAnnouncement() {
222
+ if (!this.udpSocket)
223
+ return;
224
+ const buf = Buffer.alloc(ANNOUNCEMENT_SIZE);
225
+ let offset = 0;
226
+ buf.writeUInt32BE(MAGIC, offset);
227
+ offset += 4;
228
+ Buffer.from(this.deviceId).copy(buf, offset, 0, DEVICE_ID_LENGTH);
229
+ offset += DEVICE_ID_LENGTH;
230
+ buf.writeUInt16BE(this.tcpPort, offset);
231
+ this.udpSocket.send(buf, 0, buf.length, this.udpPort, '255.255.255.255', (err) => {
232
+ if (err && this.running) {
233
+ // Best-effort; swallow transient send errors.
234
+ }
235
+ });
236
+ }
237
+ /** Process an incoming UDP announcement. */
238
+ handleAnnouncement(msg, senderAddress) {
239
+ if (msg.length < ANNOUNCEMENT_SIZE)
240
+ return;
241
+ const magic = msg.readUInt32BE(0);
242
+ if (magic !== MAGIC)
243
+ return;
244
+ const peerIdBytes = msg.subarray(4, 4 + DEVICE_ID_LENGTH);
245
+ const peerId = deviceIdToHex(peerIdBytes);
246
+ // Ignore our own announcements
247
+ if (peerId === this.deviceIdHex)
248
+ return;
249
+ const tcpPort = msg.readUInt16BE(4 + DEVICE_ID_LENGTH);
250
+ const existing = this.discoveredPeers.get(peerId);
251
+ if (existing) {
252
+ existing.lastSeen = Date.now();
253
+ existing.address = senderAddress;
254
+ existing.port = tcpPort;
255
+ }
256
+ else {
257
+ this.discoveredPeers.set(peerId, {
258
+ id: peerId,
259
+ address: senderAddress,
260
+ port: tcpPort,
261
+ lastSeen: Date.now(),
262
+ });
263
+ }
264
+ // Auto-connect if we don't already have a TCP connection
265
+ if (!this.connections.has(peerId) && !this.pendingConnections.has(peerId)) {
266
+ this.connectToPeer(senderAddress, tcpPort).catch(() => {
267
+ // Connection failed; will retry on next announcement.
268
+ });
269
+ }
270
+ }
271
+ /** Remove peers whose last announcement is older than PEER_TTL_MS. */
272
+ pruneStalePeers() {
273
+ const now = Date.now();
274
+ for (const [id, peer] of this.discoveredPeers) {
275
+ if (now - peer.lastSeen > PEER_TTL_MS) {
276
+ this.discoveredPeers.delete(id);
277
+ // Also tear down stale TCP connections
278
+ const conn = this.connections.get(id);
279
+ if (conn) {
280
+ conn.socket.destroy();
281
+ this.connections.delete(id);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ // --------------------------------------------------------
287
+ // TCP Data Channel
288
+ // --------------------------------------------------------
289
+ async startListener(port) {
290
+ const listenPort = port ?? this.tcpPort;
291
+ await new Promise((resolve, reject) => {
292
+ this.tcpServer = net.createServer((socket) => {
293
+ this.handleIncomingConnection(socket);
294
+ });
295
+ this.tcpServer.on('error', (err) => {
296
+ reject(err);
297
+ });
298
+ this.tcpServer.listen(listenPort, () => {
299
+ resolve();
300
+ });
301
+ });
302
+ }
303
+ async connectToPeer(address, port) {
304
+ // Derive a temporary key until the peer identifies itself via handshake.
305
+ const addrKey = `${address}:${port}`;
306
+ this.pendingConnections.add(addrKey);
307
+ return new Promise((resolve, reject) => {
308
+ const socket = net.createConnection({ host: address, port }, () => {
309
+ // Send our device ID so the remote side knows who connected
310
+ const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
311
+ idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
312
+ Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
313
+ socket.write(idFrame);
314
+ // We don't yet know the peer ID. We'll register the connection
315
+ // once we receive the peer's ID frame back.
316
+ const conn = {
317
+ peerId: '', // will be populated
318
+ socket,
319
+ recvBuffer: Buffer.alloc(0),
320
+ };
321
+ this.setupTcpFraming(conn, true);
322
+ this.pendingConnections.delete(addrKey);
323
+ resolve();
324
+ });
325
+ socket.on('error', (err) => {
326
+ this.pendingConnections.delete(addrKey);
327
+ reject(err);
328
+ });
329
+ });
330
+ }
331
+ /** Handle an incoming TCP connection from a remote peer. */
332
+ handleIncomingConnection(socket) {
333
+ const conn = {
334
+ peerId: '', // unknown until the peer sends its ID frame
335
+ socket,
336
+ recvBuffer: Buffer.alloc(0),
337
+ };
338
+ // The first framed message from the connecting side is the device ID.
339
+ this.setupTcpFraming(conn, false);
340
+ // Send our own ID back so the remote side can register us.
341
+ const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
342
+ idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
343
+ Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
344
+ socket.write(idFrame);
345
+ }
346
+ /**
347
+ * Attach length-prefixed framing to a TCP connection.
348
+ *
349
+ * The first message on every connection is a 16-byte device ID used to
350
+ * register the peer. All subsequent messages are serialized Packets.
351
+ *
352
+ * @param conn The peer connection wrapper (mutated in place).
353
+ * @param isInitiator True if we initiated the connection.
354
+ */
355
+ setupTcpFraming(conn, isInitiator) {
356
+ let identified = false;
357
+ conn.socket.on('data', (chunk) => {
358
+ conn.recvBuffer = Buffer.concat([conn.recvBuffer, chunk]);
359
+ // Process as many complete frames as available
360
+ while (conn.recvBuffer.length >= LENGTH_PREFIX_SIZE) {
361
+ const frameLen = conn.recvBuffer.readUInt32BE(0);
362
+ // Guard against absurdly large frames (16 MiB limit)
363
+ if (frameLen > 16 * 1024 * 1024) {
364
+ conn.socket.destroy(new Error('Frame too large'));
365
+ return;
366
+ }
367
+ if (conn.recvBuffer.length < LENGTH_PREFIX_SIZE + frameLen) {
368
+ break; // wait for more data
369
+ }
370
+ const frameData = conn.recvBuffer.subarray(LENGTH_PREFIX_SIZE, LENGTH_PREFIX_SIZE + frameLen);
371
+ conn.recvBuffer = Buffer.from(conn.recvBuffer.subarray(LENGTH_PREFIX_SIZE + frameLen));
372
+ if (!identified) {
373
+ // First frame: device ID
374
+ if (frameData.length !== DEVICE_ID_LENGTH) {
375
+ conn.socket.destroy(new Error('Invalid identification frame'));
376
+ return;
377
+ }
378
+ const peerId = deviceIdToHex(frameData);
379
+ // Don't connect to ourselves
380
+ if (peerId === this.deviceIdHex) {
381
+ conn.socket.destroy();
382
+ return;
383
+ }
384
+ // If we already have a connection to this peer, keep only one.
385
+ // The tie-breaker: the side with the lexicographically smaller ID
386
+ // keeps its *initiated* connection.
387
+ const existingConn = this.connections.get(peerId);
388
+ if (existingConn) {
389
+ const weAreSmaller = this.deviceIdHex < peerId;
390
+ if (isInitiator === weAreSmaller) {
391
+ // We keep this connection; destroy the old one.
392
+ existingConn.socket.destroy();
393
+ }
394
+ else {
395
+ // We keep the existing connection; destroy this one.
396
+ conn.socket.destroy();
397
+ return;
398
+ }
399
+ }
400
+ conn.peerId = peerId;
401
+ this.connections.set(peerId, conn);
402
+ identified = true;
403
+ }
404
+ else {
405
+ // Subsequent frames: Packets
406
+ try {
407
+ const packet = deserializePacket(Buffer.from(frameData));
408
+ this.receiveCallback?.(packet, conn.peerId);
409
+ }
410
+ catch {
411
+ // Malformed packet — drop silently.
412
+ }
413
+ }
414
+ }
415
+ });
416
+ conn.socket.on('close', () => {
417
+ if (conn.peerId && this.connections.get(conn.peerId) === conn) {
418
+ this.connections.delete(conn.peerId);
419
+ }
420
+ });
421
+ conn.socket.on('error', () => {
422
+ // Error is followed by close; cleanup happens there.
423
+ });
424
+ }
425
+ // --------------------------------------------------------
426
+ // Peer Queries
427
+ // --------------------------------------------------------
428
+ /** Return the list of peers discovered via UDP announcements. */
429
+ getDiscoveredPeers() {
430
+ return Array.from(this.discoveredPeers.values()).map(({ id, address, port }) => ({
431
+ id,
432
+ address,
433
+ port,
434
+ }));
435
+ }
436
+ /** Return the IDs of peers with an active TCP connection. */
437
+ getConnectedPeers() {
438
+ return Array.from(this.connections.keys());
439
+ }
440
+ }
441
+ export default LocalTransport;
442
+ //# sourceMappingURL=index.js.map