react-native-ble-mesh 2.0.0 → 2.1.2
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.
- package/README.md +2 -2
- package/docs/OPTIMIZATION.md +165 -52
- package/package.json +1 -1
- package/src/MeshNetwork.js +63 -53
- package/src/constants/audio.js +4 -4
- package/src/constants/ble.js +1 -1
- package/src/constants/crypto.js +1 -1
- package/src/constants/errors.js +2 -2
- package/src/constants/events.js +1 -1
- package/src/constants/protocol.js +2 -2
- package/src/crypto/AutoCrypto.js +16 -3
- package/src/crypto/CryptoProvider.js +17 -17
- package/src/crypto/providers/ExpoCryptoProvider.js +15 -9
- package/src/crypto/providers/QuickCryptoProvider.js +41 -12
- package/src/crypto/providers/TweetNaClProvider.js +10 -8
- package/src/errors/AudioError.js +2 -1
- package/src/errors/ConnectionError.js +2 -2
- package/src/errors/CryptoError.js +1 -1
- package/src/errors/HandshakeError.js +2 -2
- package/src/errors/MeshError.js +4 -4
- package/src/errors/MessageError.js +2 -2
- package/src/errors/ValidationError.js +3 -3
- package/src/expo/withBLEMesh.js +10 -10
- package/src/hooks/AppStateManager.js +11 -2
- package/src/hooks/useMesh.js +23 -10
- package/src/hooks/useMessages.js +17 -16
- package/src/hooks/usePeers.js +19 -14
- package/src/index.js +2 -2
- package/src/mesh/dedup/BloomFilter.js +45 -57
- package/src/mesh/dedup/DedupManager.js +36 -8
- package/src/mesh/dedup/MessageCache.js +3 -0
- package/src/mesh/fragment/Assembler.js +5 -4
- package/src/mesh/fragment/Fragmenter.js +3 -3
- package/src/mesh/monitor/ConnectionQuality.js +59 -25
- package/src/mesh/monitor/NetworkMonitor.js +80 -28
- package/src/mesh/peer/Peer.js +9 -11
- package/src/mesh/peer/PeerDiscovery.js +18 -19
- package/src/mesh/peer/PeerManager.js +29 -17
- package/src/mesh/router/MessageRouter.js +28 -20
- package/src/mesh/router/PathFinder.js +10 -13
- package/src/mesh/router/RouteTable.js +25 -14
- package/src/mesh/store/StoreAndForwardManager.js +32 -24
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +18 -14
- package/src/protocol/serializer.js +9 -12
- package/src/protocol/validator.js +29 -10
- package/src/service/BatteryOptimizer.js +22 -18
- package/src/service/EmergencyManager.js +18 -25
- package/src/service/HandshakeManager.js +112 -18
- package/src/service/MeshService.js +106 -22
- package/src/service/SessionManager.js +50 -13
- package/src/service/audio/AudioManager.js +80 -38
- package/src/service/audio/buffer/FrameBuffer.js +7 -8
- package/src/service/audio/buffer/JitterBuffer.js +1 -1
- package/src/service/audio/codec/LC3Codec.js +18 -19
- package/src/service/audio/codec/LC3Decoder.js +10 -10
- package/src/service/audio/codec/LC3Encoder.js +11 -9
- package/src/service/audio/session/AudioSession.js +14 -17
- package/src/service/audio/session/VoiceMessage.js +15 -22
- package/src/service/audio/transport/AudioFragmenter.js +17 -9
- package/src/service/audio/transport/AudioFramer.js +8 -12
- package/src/service/file/FileAssembler.js +4 -2
- package/src/service/file/FileChunker.js +1 -1
- package/src/service/file/FileManager.js +26 -20
- package/src/service/file/FileMessage.js +7 -12
- package/src/service/text/TextManager.js +75 -42
- package/src/service/text/broadcast/BroadcastManager.js +14 -17
- package/src/service/text/channel/Channel.js +10 -14
- package/src/service/text/channel/ChannelManager.js +10 -10
- package/src/service/text/message/TextMessage.js +12 -19
- package/src/service/text/message/TextSerializer.js +2 -2
- package/src/storage/AsyncStorageAdapter.js +17 -14
- package/src/storage/MemoryStorage.js +11 -8
- package/src/storage/MessageStore.js +77 -32
- package/src/storage/Storage.js +9 -9
- package/src/transport/BLETransport.js +27 -16
- package/src/transport/MockTransport.js +7 -2
- package/src/transport/MultiTransport.js +43 -11
- package/src/transport/Transport.js +9 -9
- package/src/transport/WiFiDirectTransport.js +26 -20
- package/src/transport/adapters/BLEAdapter.js +19 -19
- package/src/transport/adapters/NodeBLEAdapter.js +24 -23
- package/src/transport/adapters/RNBLEAdapter.js +14 -11
- package/src/utils/EventEmitter.js +15 -16
- package/src/utils/LRUCache.js +10 -4
- package/src/utils/RateLimiter.js +1 -1
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +10 -8
- package/src/utils/encoding.js +39 -8
- package/src/utils/retry.js +11 -13
- package/src/utils/time.js +9 -4
- package/src/utils/validation.js +1 -1
|
@@ -21,15 +21,27 @@ const STATE = Object.freeze({
|
|
|
21
21
|
* @extends EventEmitter
|
|
22
22
|
*/
|
|
23
23
|
class HandshakeManager extends EventEmitter {
|
|
24
|
+
/**
|
|
25
|
+
* @param {any} keyManager
|
|
26
|
+
* @param {any} sessionManager
|
|
27
|
+
*/
|
|
24
28
|
constructor(keyManager, sessionManager) {
|
|
25
29
|
super();
|
|
26
30
|
if (!keyManager || !sessionManager) { throw new Error('keyManager and sessionManager required'); }
|
|
31
|
+
/** @type {any} */
|
|
27
32
|
this._keyManager = keyManager;
|
|
33
|
+
/** @type {any} */
|
|
28
34
|
this._sessionManager = sessionManager;
|
|
35
|
+
/** @type {Map<string, any>} */
|
|
29
36
|
this._pending = new Map();
|
|
37
|
+
/** @type {number} */
|
|
30
38
|
this._timeout = MESH_CONFIG.HANDSHAKE_TIMEOUT_MS;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} peerId
|
|
43
|
+
* @param {any} transport
|
|
44
|
+
*/
|
|
33
45
|
async initiateHandshake(peerId, transport) {
|
|
34
46
|
if (this._pending.has(peerId)) { throw HandshakeError.alreadyInProgress(peerId); }
|
|
35
47
|
|
|
@@ -44,12 +56,18 @@ class HandshakeManager extends EventEmitter {
|
|
|
44
56
|
hs.step = 1;
|
|
45
57
|
this.emit(EVENTS.HANDSHAKE_PROGRESS, { peerId, step: 1, role: 'initiator' });
|
|
46
58
|
return await this._waitForCompletion(peerId);
|
|
47
|
-
} catch (err) {
|
|
59
|
+
} catch (/** @type {any} */ err) {
|
|
48
60
|
this._fail(peerId, err);
|
|
49
61
|
throw err;
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} peerId
|
|
67
|
+
* @param {number} type
|
|
68
|
+
* @param {Uint8Array} payload
|
|
69
|
+
* @param {any} transport
|
|
70
|
+
*/
|
|
53
71
|
async handleIncomingHandshake(peerId, type, payload, transport) {
|
|
54
72
|
try {
|
|
55
73
|
if (type === MESSAGE_TYPE.HANDSHAKE_INIT) {
|
|
@@ -62,12 +80,15 @@ class HandshakeManager extends EventEmitter {
|
|
|
62
80
|
return await this._onFinal(peerId, payload);
|
|
63
81
|
}
|
|
64
82
|
throw HandshakeError.handshakeFailed(peerId, null, { reason: 'Unknown type' });
|
|
65
|
-
} catch (err) {
|
|
83
|
+
} catch (/** @type {any} */ err) {
|
|
66
84
|
this._fail(peerId, err);
|
|
67
85
|
throw err;
|
|
68
86
|
}
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} peerId
|
|
91
|
+
*/
|
|
71
92
|
cancelHandshake(peerId) {
|
|
72
93
|
const hs = this._pending.get(peerId);
|
|
73
94
|
if (!hs) { return; }
|
|
@@ -77,9 +98,20 @@ class HandshakeManager extends EventEmitter {
|
|
|
77
98
|
this.emit(EVENTS.HANDSHAKE_FAILED, { peerId, reason: 'cancelled' });
|
|
78
99
|
}
|
|
79
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} peerId
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
80
105
|
isHandshakePending(peerId) { return this._pending.has(peerId); }
|
|
106
|
+
/** @returns {number} */
|
|
81
107
|
getPendingCount() { return this._pending.size; }
|
|
82
108
|
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} peerId
|
|
111
|
+
* @param {boolean} isInitiator
|
|
112
|
+
* @returns {any}
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
83
115
|
_createState(peerId, isInitiator) {
|
|
84
116
|
const kp = this._keyManager.getStaticKeyPair();
|
|
85
117
|
return {
|
|
@@ -88,11 +120,18 @@ class HandshakeManager extends EventEmitter {
|
|
|
88
120
|
};
|
|
89
121
|
}
|
|
90
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @param {any} keyPair
|
|
125
|
+
* @param {boolean} isInitiator
|
|
126
|
+
* @returns {any}
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
91
129
|
_createNoise(keyPair, isInitiator) {
|
|
92
130
|
// Get crypto provider from keyManager if available
|
|
93
131
|
const provider = this._keyManager.provider;
|
|
94
132
|
|
|
95
133
|
// Generate ephemeral key pair for this handshake
|
|
134
|
+
/** @type {any} */
|
|
96
135
|
let ephemeralKeyPair;
|
|
97
136
|
if (provider && typeof provider.generateKeyPair === 'function') {
|
|
98
137
|
ephemeralKeyPair = provider.generateKeyPair();
|
|
@@ -101,11 +140,18 @@ class HandshakeManager extends EventEmitter {
|
|
|
101
140
|
ephemeralKeyPair = { publicKey: randomBytes(32), secretKey: randomBytes(32) };
|
|
102
141
|
}
|
|
103
142
|
|
|
143
|
+
/** @type {Uint8Array | null} */
|
|
104
144
|
let remoteEphemeralPublic = null;
|
|
145
|
+
/** @type {Uint8Array | null} */
|
|
105
146
|
let sharedSecret = null;
|
|
147
|
+
/** @type {any} */
|
|
106
148
|
let sessionKeys = null;
|
|
107
149
|
let complete = false;
|
|
108
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {Uint8Array} secret
|
|
153
|
+
* @returns {any}
|
|
154
|
+
*/
|
|
109
155
|
const deriveSessionKeys = (secret) => {
|
|
110
156
|
// Derive send/receive keys from shared secret
|
|
111
157
|
// Use hash to derive two different keys from the secret
|
|
@@ -125,6 +171,7 @@ class HandshakeManager extends EventEmitter {
|
|
|
125
171
|
return ephemeralKeyPair.publicKey;
|
|
126
172
|
},
|
|
127
173
|
|
|
174
|
+
/** @param {Uint8Array} msg */
|
|
128
175
|
readMessage1: (msg) => {
|
|
129
176
|
// Responder receives initiator's ephemeral public key
|
|
130
177
|
remoteEphemeralPublic = new Uint8Array(msg);
|
|
@@ -137,10 +184,12 @@ class HandshakeManager extends EventEmitter {
|
|
|
137
184
|
} else {
|
|
138
185
|
throw new Error('Crypto provider required for secure handshake. Install tweetnacl: npm install tweetnacl');
|
|
139
186
|
}
|
|
187
|
+
// @ts-ignore
|
|
140
188
|
sessionKeys = deriveSessionKeys(sharedSecret);
|
|
141
189
|
return ephemeralKeyPair.publicKey;
|
|
142
190
|
},
|
|
143
191
|
|
|
192
|
+
/** @param {Uint8Array} msg */
|
|
144
193
|
readMessage2: (msg) => {
|
|
145
194
|
// Initiator receives responder's ephemeral public key and derives shared secret
|
|
146
195
|
remoteEphemeralPublic = new Uint8Array(msg);
|
|
@@ -149,6 +198,7 @@ class HandshakeManager extends EventEmitter {
|
|
|
149
198
|
} else {
|
|
150
199
|
throw new Error('Crypto provider required for secure handshake. Install tweetnacl: npm install tweetnacl');
|
|
151
200
|
}
|
|
201
|
+
// @ts-ignore
|
|
152
202
|
sessionKeys = deriveSessionKeys(sharedSecret);
|
|
153
203
|
},
|
|
154
204
|
|
|
@@ -158,6 +208,7 @@ class HandshakeManager extends EventEmitter {
|
|
|
158
208
|
return ephemeralKeyPair.publicKey;
|
|
159
209
|
},
|
|
160
210
|
|
|
211
|
+
/** @param {any} _msg */
|
|
161
212
|
readMessage3: (_msg) => {
|
|
162
213
|
// Responder confirms handshake completion
|
|
163
214
|
complete = true;
|
|
@@ -177,32 +228,30 @@ class HandshakeManager extends EventEmitter {
|
|
|
177
228
|
let sendNonce = 0;
|
|
178
229
|
let recvNonce = 0;
|
|
179
230
|
|
|
231
|
+
// Pre-allocate nonce buffers per direction to avoid per-call allocation
|
|
232
|
+
const sendNonceBuf = new Uint8Array(24);
|
|
233
|
+
const sendNonceView = new DataView(sendNonceBuf.buffer);
|
|
234
|
+
const recvNonceBuf = new Uint8Array(24);
|
|
235
|
+
const recvNonceView = new DataView(recvNonceBuf.buffer);
|
|
236
|
+
|
|
180
237
|
return {
|
|
238
|
+
/** @param {Uint8Array} plaintext */
|
|
181
239
|
encrypt: (plaintext) => {
|
|
182
240
|
if (provider && typeof provider.encrypt === 'function') {
|
|
183
|
-
// Use proper AEAD encryption with nonce
|
|
184
|
-
const nonce = new Uint8Array(24); // tweetnacl uses 24-byte nonces
|
|
185
|
-
const view = new DataView(nonce.buffer);
|
|
186
241
|
// Store counter in last 8 bytes of nonce
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
return provider.encrypt(sendKey, nonce, plaintext);
|
|
242
|
+
sendNonceView.setUint32(16, 0, true);
|
|
243
|
+
sendNonceView.setUint32(20, sendNonce++, true);
|
|
244
|
+
return provider.encrypt(sendKey, sendNonceBuf, plaintext);
|
|
192
245
|
}
|
|
193
246
|
throw new Error('Crypto provider required for encryption');
|
|
194
247
|
},
|
|
195
248
|
|
|
249
|
+
/** @param {Uint8Array} ciphertext */
|
|
196
250
|
decrypt: (ciphertext) => {
|
|
197
251
|
if (provider && typeof provider.decrypt === 'function') {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (nonce.byteLength >= 8) {
|
|
202
|
-
view.setUint32(nonce.byteLength - 8, 0, true);
|
|
203
|
-
view.setUint32(nonce.byteLength - 4, recvNonce++, true);
|
|
204
|
-
}
|
|
205
|
-
return provider.decrypt(recvKey, nonce, ciphertext);
|
|
252
|
+
recvNonceView.setUint32(16, 0, true);
|
|
253
|
+
recvNonceView.setUint32(20, recvNonce++, true);
|
|
254
|
+
return provider.decrypt(recvKey, recvNonceBuf, ciphertext);
|
|
206
255
|
}
|
|
207
256
|
throw new Error('Crypto provider required for encryption');
|
|
208
257
|
},
|
|
@@ -219,6 +268,12 @@ class HandshakeManager extends EventEmitter {
|
|
|
219
268
|
};
|
|
220
269
|
}
|
|
221
270
|
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} peerId
|
|
273
|
+
* @param {Uint8Array} payload
|
|
274
|
+
* @param {any} transport
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
222
277
|
async _onInit(peerId, payload, transport) {
|
|
223
278
|
const existing = this._pending.get(peerId);
|
|
224
279
|
|
|
@@ -249,6 +304,12 @@ class HandshakeManager extends EventEmitter {
|
|
|
249
304
|
return null;
|
|
250
305
|
}
|
|
251
306
|
|
|
307
|
+
/**
|
|
308
|
+
* @param {string} peerId
|
|
309
|
+
* @param {Uint8Array} payload
|
|
310
|
+
* @param {any} transport
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
252
313
|
async _onResponse(peerId, payload, transport) {
|
|
253
314
|
const hs = this._pending.get(peerId);
|
|
254
315
|
if (!hs || !hs.isInitiator) {
|
|
@@ -262,6 +323,11 @@ class HandshakeManager extends EventEmitter {
|
|
|
262
323
|
return this._complete(peerId, hs);
|
|
263
324
|
}
|
|
264
325
|
|
|
326
|
+
/**
|
|
327
|
+
* @param {string} peerId
|
|
328
|
+
* @param {Uint8Array} payload
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
265
331
|
async _onFinal(peerId, payload) {
|
|
266
332
|
const hs = this._pending.get(peerId);
|
|
267
333
|
if (!hs || hs.isInitiator) { throw HandshakeError.invalidState(peerId, 3); }
|
|
@@ -270,6 +336,11 @@ class HandshakeManager extends EventEmitter {
|
|
|
270
336
|
return this._complete(peerId, hs);
|
|
271
337
|
}
|
|
272
338
|
|
|
339
|
+
/**
|
|
340
|
+
* @param {string} peerId
|
|
341
|
+
* @param {any} hs
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
273
344
|
_complete(peerId, hs) {
|
|
274
345
|
if (hs.timer) { clearTimeout(hs.timer); }
|
|
275
346
|
const session = hs.noise.getSession();
|
|
@@ -283,6 +354,11 @@ class HandshakeManager extends EventEmitter {
|
|
|
283
354
|
return session;
|
|
284
355
|
}
|
|
285
356
|
|
|
357
|
+
/**
|
|
358
|
+
* @param {string} peerId
|
|
359
|
+
* @param {any} error
|
|
360
|
+
* @private
|
|
361
|
+
*/
|
|
286
362
|
_fail(peerId, error) {
|
|
287
363
|
const hs = this._pending.get(peerId);
|
|
288
364
|
if (hs) {
|
|
@@ -293,6 +369,10 @@ class HandshakeManager extends EventEmitter {
|
|
|
293
369
|
this.emit(EVENTS.HANDSHAKE_FAILED, { peerId, error: error.message, step: hs?.step });
|
|
294
370
|
}
|
|
295
371
|
|
|
372
|
+
/**
|
|
373
|
+
* @param {string} peerId
|
|
374
|
+
* @private
|
|
375
|
+
*/
|
|
296
376
|
_setTimeout(peerId) {
|
|
297
377
|
const hs = this._pending.get(peerId);
|
|
298
378
|
if (!hs) { return; }
|
|
@@ -303,6 +383,10 @@ class HandshakeManager extends EventEmitter {
|
|
|
303
383
|
}, this._timeout);
|
|
304
384
|
}
|
|
305
385
|
|
|
386
|
+
/**
|
|
387
|
+
* @param {string} peerId
|
|
388
|
+
* @private
|
|
389
|
+
*/
|
|
306
390
|
_waitForCompletion(peerId) {
|
|
307
391
|
return new Promise((resolve, reject) => {
|
|
308
392
|
const hs = this._pending.get(peerId);
|
|
@@ -313,6 +397,11 @@ class HandshakeManager extends EventEmitter {
|
|
|
313
397
|
});
|
|
314
398
|
}
|
|
315
399
|
|
|
400
|
+
/**
|
|
401
|
+
* @param {any} localKey
|
|
402
|
+
* @param {any} remoteId
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
316
405
|
_compareKeys(localKey, remoteId) {
|
|
317
406
|
// Simple string/byte comparison for deterministic tie-breaking
|
|
318
407
|
const localStr = typeof localKey === 'string' ? localKey : Array.from(localKey).join(',');
|
|
@@ -320,6 +409,11 @@ class HandshakeManager extends EventEmitter {
|
|
|
320
409
|
return localStr < remoteStr ? -1 : localStr > remoteStr ? 1 : 0;
|
|
321
410
|
}
|
|
322
411
|
|
|
412
|
+
/**
|
|
413
|
+
* @param {number} type
|
|
414
|
+
* @param {Uint8Array} payload
|
|
415
|
+
* @private
|
|
416
|
+
*/
|
|
323
417
|
_wrap(type, payload) {
|
|
324
418
|
const w = new Uint8Array(1 + payload.length);
|
|
325
419
|
w[0] = type;
|
|
@@ -19,21 +19,38 @@ const { TextManager, ChannelManager } = require('./text');
|
|
|
19
19
|
* @extends EventEmitter
|
|
20
20
|
*/
|
|
21
21
|
class MeshService extends EventEmitter {
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} [config]
|
|
24
|
+
*/
|
|
22
25
|
constructor(config = {}) {
|
|
23
26
|
super();
|
|
27
|
+
/** @type {any} */
|
|
24
28
|
this._config = { displayName: 'Anonymous', ...config };
|
|
29
|
+
/** @type {string} */
|
|
25
30
|
this._state = SERVICE_STATE.UNINITIALIZED;
|
|
31
|
+
/** @type {any} */
|
|
26
32
|
this._transport = null;
|
|
33
|
+
/** @type {any} */
|
|
27
34
|
this._keyManager = null;
|
|
35
|
+
/** @type {SessionManager | null} */
|
|
28
36
|
this._sessionManager = null;
|
|
37
|
+
/** @type {HandshakeManager | null} */
|
|
29
38
|
this._handshakeManager = null;
|
|
39
|
+
/** @type {any} */
|
|
30
40
|
this._channelManager = null;
|
|
41
|
+
/** @type {any} */
|
|
31
42
|
this._peerManager = null;
|
|
43
|
+
/** @type {any} */
|
|
32
44
|
this._audioManager = null;
|
|
45
|
+
/** @type {any} */
|
|
33
46
|
this._textManager = null;
|
|
47
|
+
/** @type {number} */
|
|
34
48
|
this._messageCounter = 0;
|
|
35
49
|
}
|
|
36
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {any} [options]
|
|
53
|
+
*/
|
|
37
54
|
async initialize(options = {}) {
|
|
38
55
|
if (this._state !== SERVICE_STATE.UNINITIALIZED) {
|
|
39
56
|
throw new MeshError('Service already initialized', ERROR_CODE.E002);
|
|
@@ -47,12 +64,15 @@ class MeshService extends EventEmitter {
|
|
|
47
64
|
this._setupEventForwarding();
|
|
48
65
|
this._setState(SERVICE_STATE.READY);
|
|
49
66
|
this.emit(EVENTS.INITIALIZED);
|
|
50
|
-
} catch (err) {
|
|
67
|
+
} catch (/** @type {any} */ err) {
|
|
51
68
|
this._setState(SERVICE_STATE.ERROR);
|
|
52
69
|
throw new MeshError(`Initialization failed: ${err.message}`, ERROR_CODE.E001);
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {any} transport
|
|
75
|
+
*/
|
|
56
76
|
async start(transport) {
|
|
57
77
|
this._validateState([SERVICE_STATE.READY, SERVICE_STATE.SUSPENDED]);
|
|
58
78
|
if (!transport) { throw new ValidationError('Transport is required', ERROR_CODE.E802); }
|
|
@@ -87,31 +107,52 @@ class MeshService extends EventEmitter {
|
|
|
87
107
|
};
|
|
88
108
|
}
|
|
89
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} name
|
|
112
|
+
*/
|
|
90
113
|
setDisplayName(name) { this._config.displayName = name; }
|
|
91
114
|
exportIdentity() { return this._keyManager?.exportIdentity() || null; }
|
|
115
|
+
/**
|
|
116
|
+
* @param {any} identity
|
|
117
|
+
*/
|
|
92
118
|
importIdentity(identity) { this._keyManager?.importIdentity(identity); }
|
|
93
119
|
|
|
94
120
|
getPeers() { return this._peerManager?.getAllPeers() || []; }
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} id
|
|
123
|
+
*/
|
|
95
124
|
getPeer(id) { return this._peerManager?.getPeer(id); }
|
|
96
125
|
getConnectedPeers() { return this._peerManager?.getConnectedPeers() || []; }
|
|
97
126
|
getSecuredPeers() { return this._sessionManager?.getAllSessionPeerIds() || []; }
|
|
98
127
|
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} peerId
|
|
130
|
+
*/
|
|
99
131
|
async initiateHandshake(peerId) {
|
|
100
132
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
101
|
-
return this._handshakeManager
|
|
133
|
+
return this._handshakeManager?.initiateHandshake(peerId, this._transport);
|
|
102
134
|
}
|
|
103
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} id
|
|
138
|
+
*/
|
|
104
139
|
blockPeer(id) {
|
|
105
140
|
this._peerManager?.blockPeer(id);
|
|
106
141
|
this.emit(EVENTS.PEER_BLOCKED, { peerId: id });
|
|
107
142
|
}
|
|
108
143
|
|
|
144
|
+
/**
|
|
145
|
+
* @param {string} id
|
|
146
|
+
*/
|
|
109
147
|
unblockPeer(id) {
|
|
110
148
|
this._peerManager?.unblockPeer(id);
|
|
111
149
|
this.emit(EVENTS.PEER_UNBLOCKED, { peerId: id });
|
|
112
150
|
}
|
|
113
151
|
|
|
114
152
|
// Text messaging methods
|
|
153
|
+
/**
|
|
154
|
+
* @param {string} content
|
|
155
|
+
*/
|
|
115
156
|
sendBroadcast(content) {
|
|
116
157
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
117
158
|
if (this._textManager) {
|
|
@@ -130,26 +171,34 @@ class MeshService extends EventEmitter {
|
|
|
130
171
|
return messageId;
|
|
131
172
|
}
|
|
132
173
|
|
|
174
|
+
/**
|
|
175
|
+
* @param {string} peerId
|
|
176
|
+
* @param {string} content
|
|
177
|
+
*/
|
|
133
178
|
async sendPrivateMessage(peerId, content) {
|
|
134
179
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
135
180
|
if (this._textManager) {
|
|
136
181
|
return this._textManager.sendPrivateMessage(peerId, content);
|
|
137
182
|
}
|
|
138
|
-
if (!this._sessionManager
|
|
183
|
+
if (!this._sessionManager?.hasSession(peerId)) { await this.initiateHandshake(peerId); }
|
|
139
184
|
const messageId = this._generateMessageId();
|
|
140
185
|
const plaintext = new TextEncoder().encode(content);
|
|
141
|
-
const ciphertext = this._sessionManager
|
|
186
|
+
const ciphertext = this._sessionManager?.encryptFor(peerId, plaintext);
|
|
142
187
|
await this._transport.send(peerId, ciphertext);
|
|
143
188
|
this.emit(EVENTS.PRIVATE_MESSAGE_SENT, { messageId, peerId });
|
|
144
189
|
return messageId;
|
|
145
190
|
}
|
|
146
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {string} channelId
|
|
194
|
+
* @param {string} content
|
|
195
|
+
*/
|
|
147
196
|
sendChannelMessage(channelId, content) {
|
|
148
197
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
149
198
|
if (this._textManager) {
|
|
150
199
|
return this._textManager.sendChannelMessage(channelId, content);
|
|
151
200
|
}
|
|
152
|
-
if (!this._channelManager
|
|
201
|
+
if (!this._channelManager?.isInChannel(channelId)) {
|
|
153
202
|
throw new MeshError('Not in channel', ERROR_CODE.E602);
|
|
154
203
|
}
|
|
155
204
|
const messageId = this._generateMessageId();
|
|
@@ -157,28 +206,38 @@ class MeshService extends EventEmitter {
|
|
|
157
206
|
return messageId;
|
|
158
207
|
}
|
|
159
208
|
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} channelId
|
|
211
|
+
* @param {string} [password]
|
|
212
|
+
*/
|
|
160
213
|
joinChannel(channelId, password) {
|
|
161
214
|
if (this._textManager) {
|
|
162
215
|
return this._textManager.joinChannel(channelId, password);
|
|
163
216
|
}
|
|
164
|
-
this._channelManager
|
|
217
|
+
this._channelManager?.joinChannel(channelId, password);
|
|
165
218
|
}
|
|
166
219
|
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} channelId
|
|
222
|
+
*/
|
|
167
223
|
leaveChannel(channelId) {
|
|
168
224
|
if (this._textManager) {
|
|
169
225
|
return this._textManager.leaveChannel(channelId);
|
|
170
226
|
}
|
|
171
|
-
this._channelManager
|
|
227
|
+
this._channelManager?.leaveChannel(channelId);
|
|
172
228
|
}
|
|
173
229
|
|
|
174
230
|
getChannels() {
|
|
175
231
|
if (this._textManager) {
|
|
176
232
|
return this._textManager.getChannels();
|
|
177
233
|
}
|
|
178
|
-
return this._channelManager
|
|
234
|
+
return this._channelManager?.getChannels() || [];
|
|
179
235
|
}
|
|
180
236
|
|
|
181
237
|
// Text manager methods
|
|
238
|
+
/**
|
|
239
|
+
* @param {any} [options]
|
|
240
|
+
*/
|
|
182
241
|
async initializeText(options = {}) {
|
|
183
242
|
this._validateState([SERVICE_STATE.READY, SERVICE_STATE.ACTIVE]);
|
|
184
243
|
if (this._textManager) {
|
|
@@ -192,6 +251,9 @@ class MeshService extends EventEmitter {
|
|
|
192
251
|
getTextManager() { return this._textManager; }
|
|
193
252
|
|
|
194
253
|
// Audio methods
|
|
254
|
+
/**
|
|
255
|
+
* @param {any} [options]
|
|
256
|
+
*/
|
|
195
257
|
async initializeAudio(options = {}) {
|
|
196
258
|
this._validateState([SERVICE_STATE.READY, SERVICE_STATE.ACTIVE]);
|
|
197
259
|
if (this._audioManager) {
|
|
@@ -204,6 +266,10 @@ class MeshService extends EventEmitter {
|
|
|
204
266
|
|
|
205
267
|
getAudioManager() { return this._audioManager; }
|
|
206
268
|
|
|
269
|
+
/**
|
|
270
|
+
* @param {string} peerId
|
|
271
|
+
* @param {any} voiceMessage
|
|
272
|
+
*/
|
|
207
273
|
async sendVoiceMessage(peerId, voiceMessage) {
|
|
208
274
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
209
275
|
if (!this._audioManager) {
|
|
@@ -212,6 +278,9 @@ class MeshService extends EventEmitter {
|
|
|
212
278
|
return this._audioManager.sendVoiceMessage(peerId, voiceMessage);
|
|
213
279
|
}
|
|
214
280
|
|
|
281
|
+
/**
|
|
282
|
+
* @param {string} peerId
|
|
283
|
+
*/
|
|
215
284
|
async requestAudioStream(peerId) {
|
|
216
285
|
this._validateState([SERVICE_STATE.ACTIVE]);
|
|
217
286
|
if (!this._audioManager) {
|
|
@@ -228,7 +297,7 @@ class MeshService extends EventEmitter {
|
|
|
228
297
|
EVENTS.CHANNEL_JOINED, EVENTS.CHANNEL_LEFT, EVENTS.CHANNEL_MESSAGE,
|
|
229
298
|
EVENTS.CHANNEL_MEMBER_JOINED, EVENTS.CHANNEL_MEMBER_LEFT
|
|
230
299
|
];
|
|
231
|
-
textEvents.forEach(e => this._textManager.on(e, d => this.emit(e, d)));
|
|
300
|
+
textEvents.forEach((/** @type {string} */ e) => this._textManager.on(e, (/** @type {any} */ d) => this.emit(e, d)));
|
|
232
301
|
}
|
|
233
302
|
|
|
234
303
|
_setupAudioEventForwarding() {
|
|
@@ -237,9 +306,13 @@ class MeshService extends EventEmitter {
|
|
|
237
306
|
EVENTS.AUDIO_STREAM_REQUEST, EVENTS.AUDIO_STREAM_STARTED, EVENTS.AUDIO_STREAM_ENDED,
|
|
238
307
|
EVENTS.VOICE_MESSAGE_RECEIVED, EVENTS.VOICE_MESSAGE_SENT, EVENTS.VOICE_MESSAGE_PROGRESS
|
|
239
308
|
];
|
|
240
|
-
audioEvents.forEach(e => this._audioManager.on(e, d => this.emit(e, d)));
|
|
309
|
+
audioEvents.forEach((/** @type {string} */ e) => this._audioManager.on(e, (/** @type {any} */ d) => this.emit(e, d)));
|
|
241
310
|
}
|
|
242
311
|
|
|
312
|
+
/**
|
|
313
|
+
* @param {string} peerId
|
|
314
|
+
* @param {Uint8Array} data
|
|
315
|
+
*/
|
|
243
316
|
async _sendRaw(peerId, data) {
|
|
244
317
|
if (this._state === SERVICE_STATE.DESTROYED || !this._transport) {
|
|
245
318
|
return; // Silently ignore sends after destroy
|
|
@@ -251,8 +324,8 @@ class MeshService extends EventEmitter {
|
|
|
251
324
|
return {
|
|
252
325
|
state: this._state, identity: this.getIdentity(),
|
|
253
326
|
peerCount: this.getConnectedPeers().length, securedPeerCount: this.getSecuredPeers().length,
|
|
254
|
-
channelCount: this._channelManager?.getChannels()
|
|
255
|
-
sessionCount: this._sessionManager?.getAllSessionPeerIds()
|
|
327
|
+
channelCount: this._channelManager?.getChannels()?.length || 0,
|
|
328
|
+
sessionCount: this._sessionManager?.getAllSessionPeerIds()?.length || 0,
|
|
256
329
|
hasTextManager: !!this._textManager,
|
|
257
330
|
hasAudioManager: !!this._audioManager
|
|
258
331
|
};
|
|
@@ -260,12 +333,18 @@ class MeshService extends EventEmitter {
|
|
|
260
333
|
|
|
261
334
|
getState() { return this._state; }
|
|
262
335
|
|
|
336
|
+
/**
|
|
337
|
+
* @param {string} newState
|
|
338
|
+
*/
|
|
263
339
|
_setState(newState) {
|
|
264
340
|
const oldState = this._state;
|
|
265
341
|
this._state = newState;
|
|
266
342
|
this.emit(EVENTS.STATE_CHANGED, { oldState, newState });
|
|
267
343
|
}
|
|
268
344
|
|
|
345
|
+
/**
|
|
346
|
+
* @param {string[]} allowed
|
|
347
|
+
*/
|
|
269
348
|
_validateState(allowed) {
|
|
270
349
|
if (!allowed.includes(this._state)) {
|
|
271
350
|
throw new MeshError(`Invalid state: ${this._state}`, ERROR_CODE.E003);
|
|
@@ -276,13 +355,15 @@ class MeshService extends EventEmitter {
|
|
|
276
355
|
|
|
277
356
|
_createKeyManager() {
|
|
278
357
|
const { createProvider } = require('../crypto/AutoCrypto');
|
|
358
|
+
/** @type {any} */
|
|
279
359
|
let provider;
|
|
360
|
+
/** @type {any} */
|
|
280
361
|
let keyPair;
|
|
281
362
|
|
|
282
363
|
try {
|
|
283
364
|
provider = createProvider('auto');
|
|
284
365
|
keyPair = provider.generateKeyPair();
|
|
285
|
-
} catch (e) {
|
|
366
|
+
} catch (/** @type {any} */ e) {
|
|
286
367
|
// If no crypto provider is available, return a minimal fallback
|
|
287
368
|
// that generates random keys using basic randomBytes
|
|
288
369
|
const { randomBytes } = require('../utils/bytes');
|
|
@@ -291,7 +372,7 @@ class MeshService extends EventEmitter {
|
|
|
291
372
|
getStaticKeyPair: () => keyPair,
|
|
292
373
|
getPublicKey: () => keyPair.publicKey,
|
|
293
374
|
exportIdentity: () => ({ publicKey: Array.from(keyPair.publicKey) }),
|
|
294
|
-
importIdentity: (id) => {
|
|
375
|
+
importIdentity: (/** @type {any} */ id) => {
|
|
295
376
|
if (id && id.publicKey) {
|
|
296
377
|
keyPair.publicKey = new Uint8Array(id.publicKey);
|
|
297
378
|
}
|
|
@@ -310,7 +391,7 @@ class MeshService extends EventEmitter {
|
|
|
310
391
|
publicKey: Array.from(keyPair.publicKey),
|
|
311
392
|
secretKey: Array.from(keyPair.secretKey)
|
|
312
393
|
}),
|
|
313
|
-
importIdentity: (id) => {
|
|
394
|
+
importIdentity: (/** @type {any} */ id) => {
|
|
314
395
|
if (id && id.publicKey) {
|
|
315
396
|
keyPair.publicKey = new Uint8Array(id.publicKey);
|
|
316
397
|
}
|
|
@@ -322,7 +403,7 @@ class MeshService extends EventEmitter {
|
|
|
322
403
|
}
|
|
323
404
|
|
|
324
405
|
_setupEventForwarding() {
|
|
325
|
-
const fwd = (em, evts) => evts.forEach(e => em.on(e, d => this.emit(e, d)));
|
|
406
|
+
const fwd = (/** @type {any} */ em, /** @type {string[]} */ evts) => evts.forEach((/** @type {string} */ e) => em.on(e, (/** @type {any} */ d) => this.emit(e, d)));
|
|
326
407
|
fwd(this._handshakeManager, [EVENTS.HANDSHAKE_STARTED, EVENTS.HANDSHAKE_PROGRESS,
|
|
327
408
|
EVENTS.HANDSHAKE_COMPLETE, EVENTS.HANDSHAKE_FAILED]);
|
|
328
409
|
fwd(this._channelManager, [EVENTS.CHANNEL_JOINED, EVENTS.CHANNEL_LEFT,
|
|
@@ -330,15 +411,18 @@ class MeshService extends EventEmitter {
|
|
|
330
411
|
}
|
|
331
412
|
|
|
332
413
|
_setupTransportListeners() {
|
|
333
|
-
this._transport.on('message', d => this._handleIncoming(d));
|
|
334
|
-
this._transport.on('peerConnected', d => this.emit(EVENTS.PEER_CONNECTED, d));
|
|
335
|
-
this._transport.on('peerDisconnected', d => this.emit(EVENTS.PEER_DISCONNECTED, d));
|
|
414
|
+
this._transport.on('message', (/** @type {any} */ d) => this._handleIncoming(d));
|
|
415
|
+
this._transport.on('peerConnected', (/** @type {any} */ d) => this.emit(EVENTS.PEER_CONNECTED, d));
|
|
416
|
+
this._transport.on('peerDisconnected', (/** @type {any} */ d) => this.emit(EVENTS.PEER_DISCONNECTED, d));
|
|
336
417
|
}
|
|
337
418
|
|
|
419
|
+
/**
|
|
420
|
+
* @param {any} param0
|
|
421
|
+
*/
|
|
338
422
|
_handleIncoming({ peerId, data }) {
|
|
339
423
|
const type = data[0], payload = data.subarray(1);
|
|
340
424
|
if (type >= MESSAGE_TYPE.HANDSHAKE_INIT && type <= MESSAGE_TYPE.HANDSHAKE_FINAL) {
|
|
341
|
-
this._handshakeManager
|
|
425
|
+
this._handshakeManager?.handleIncomingHandshake(peerId, type, payload, this._transport);
|
|
342
426
|
} else if (type === MESSAGE_TYPE.CHANNEL_MESSAGE) {
|
|
343
427
|
if (this._textManager) {
|
|
344
428
|
this._textManager.handleIncomingMessage(peerId, type, payload);
|
|
@@ -351,7 +435,7 @@ class MeshService extends EventEmitter {
|
|
|
351
435
|
const parsed = JSON.parse(decoded);
|
|
352
436
|
channelId = parsed.channelId || '';
|
|
353
437
|
content = parsed.content ? new TextEncoder().encode(parsed.content) : payload;
|
|
354
|
-
} catch (e) {
|
|
438
|
+
} catch (/** @type {any} */ e) {
|
|
355
439
|
// If not JSON, try to extract channelId as length-prefixed string
|
|
356
440
|
if (payload.length > 1) {
|
|
357
441
|
const channelIdLen = payload[0];
|
|
@@ -361,7 +445,7 @@ class MeshService extends EventEmitter {
|
|
|
361
445
|
}
|
|
362
446
|
}
|
|
363
447
|
}
|
|
364
|
-
this._channelManager
|
|
448
|
+
this._channelManager?.handleChannelMessage({ channelId, senderId: peerId, content });
|
|
365
449
|
}
|
|
366
450
|
} else if (type >= MESSAGE_TYPE.VOICE_MESSAGE_START && type <= MESSAGE_TYPE.AUDIO_STREAM_END) {
|
|
367
451
|
if (this._audioManager) {
|