@vailix/mask 0.2.4 → 0.2.6
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/CHANGELOG.md +16 -0
- package/__tests__/ble-error-handling.test.ts +355 -0
- package/__tests__/singleton.test.ts +4 -0
- package/dist/index.js +19 -2
- package/dist/index.mjs +24 -7
- package/package.json +1 -1
- package/src/ble.ts +19 -1
- package/src/identity.ts +2 -2
- package/src/index.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @vailix/mask
|
|
2
2
|
|
|
3
|
+
## 0.2.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 36836bb: Fix BLE error handling: verify initialization, emit async errors properly, and add connection timeout.
|
|
8
|
+
|
|
9
|
+
## 0.2.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a48760e: Fix master key generation to use cryptographically secure random bytes
|
|
14
|
+
|
|
15
|
+
Changed master key generation from `randomUUID()` to `randomBytes(32).toString('hex')`.
|
|
16
|
+
This fixes the "Invalid master key format" error caused by UUID hyphens failing hex validation,
|
|
17
|
+
and provides proper 256-bit cryptographic randomness following security best practices.
|
|
18
|
+
|
|
3
19
|
## 0.2.4
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for BleService error handling improvements.
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - EventEmitter pattern for error propagation
|
|
6
|
+
* - BLE state check before scanning (initialize() call)
|
|
7
|
+
* - Timeout behavior in initialize()
|
|
8
|
+
* - Error emission from scan callback
|
|
9
|
+
*
|
|
10
|
+
* NOTE: These tests use a simplified TestBleService that mirrors the actual
|
|
11
|
+
* BleService implementation logic without importing react-native-ble-plx.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
15
|
+
import { EventEmitter } from 'eventemitter3';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Mock BLE State enum
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const State = {
|
|
21
|
+
PoweredOn: 'PoweredOn',
|
|
22
|
+
PoweredOff: 'PoweredOff',
|
|
23
|
+
Unauthorized: 'Unauthorized',
|
|
24
|
+
Unsupported: 'Unsupported',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Mock BleManager that tracks calls
|
|
28
|
+
class MockBleManager {
|
|
29
|
+
onStateChange = vi.fn();
|
|
30
|
+
startDeviceScan = vi.fn();
|
|
31
|
+
stopDeviceScan = vi.fn();
|
|
32
|
+
destroy = vi.fn();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Simplified BleService implementation for testing the logic patterns.
|
|
37
|
+
* This mirrors the actual BleService but without the react-native-ble-plx import.
|
|
38
|
+
*
|
|
39
|
+
* Key difference from actual code: uses setImmediate/setTimeout(0) to defer
|
|
40
|
+
* synchronous callbacks, avoiding temporal dead zone issues in tests.
|
|
41
|
+
*/
|
|
42
|
+
class TestBleService extends EventEmitter {
|
|
43
|
+
private manager: MockBleManager;
|
|
44
|
+
private isScanning = false;
|
|
45
|
+
|
|
46
|
+
constructor(mockManager: MockBleManager) {
|
|
47
|
+
super();
|
|
48
|
+
this.manager = mockManager;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async initialize(): Promise<boolean> {
|
|
52
|
+
const INIT_TIMEOUT_MS = 5000;
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let resolved = false;
|
|
56
|
+
let subscriptionRef: { remove: () => void } | null = null;
|
|
57
|
+
|
|
58
|
+
const timeout = setTimeout(() => {
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
resolved = true;
|
|
61
|
+
subscriptionRef?.remove();
|
|
62
|
+
resolve(false);
|
|
63
|
+
}
|
|
64
|
+
}, INIT_TIMEOUT_MS);
|
|
65
|
+
|
|
66
|
+
const handleState = (state: string) => {
|
|
67
|
+
if (resolved) return;
|
|
68
|
+
|
|
69
|
+
if (state === State.PoweredOn) {
|
|
70
|
+
resolved = true;
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
subscriptionRef?.remove();
|
|
73
|
+
resolve(true);
|
|
74
|
+
} else if (state === State.PoweredOff || state === State.Unauthorized) {
|
|
75
|
+
resolved = true;
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
subscriptionRef?.remove();
|
|
78
|
+
resolve(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
subscriptionRef = this.manager.onStateChange(handleState, true);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async startDiscovery(myRpi: string, myMetadataKey: string): Promise<void> {
|
|
87
|
+
if (this.isScanning) return;
|
|
88
|
+
|
|
89
|
+
const isReady = await this.initialize();
|
|
90
|
+
if (!isReady) {
|
|
91
|
+
throw new Error('Bluetooth is not available or not enabled');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.isScanning = true;
|
|
95
|
+
this.startScanning();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private startScanning(): void {
|
|
99
|
+
this.manager.startDeviceScan(
|
|
100
|
+
['service-uuid'],
|
|
101
|
+
{ allowDuplicates: true },
|
|
102
|
+
(error: any, device: any) => {
|
|
103
|
+
if (error) {
|
|
104
|
+
console.warn('BLE scan error:', error);
|
|
105
|
+
this.emit('error', error);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stopDiscovery(): void {
|
|
113
|
+
this.isScanning = false;
|
|
114
|
+
this.manager.stopDeviceScan();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
destroy(): void {
|
|
118
|
+
this.stopDiscovery();
|
|
119
|
+
this.manager.destroy();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('BleService Error Handling', () => {
|
|
124
|
+
let mockManager: MockBleManager;
|
|
125
|
+
let bleService: TestBleService;
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
vi.clearAllMocks();
|
|
129
|
+
vi.useFakeTimers();
|
|
130
|
+
mockManager = new MockBleManager();
|
|
131
|
+
bleService = new TestBleService(mockManager);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
vi.useRealTimers();
|
|
136
|
+
bleService.destroy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('EventEmitter pattern', () => {
|
|
140
|
+
it('should extend EventEmitter and support on/off/emit', () => {
|
|
141
|
+
const errorHandler = vi.fn();
|
|
142
|
+
|
|
143
|
+
bleService.on('error', errorHandler);
|
|
144
|
+
bleService.emit('error', new Error('test error'));
|
|
145
|
+
|
|
146
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should allow removing listeners', () => {
|
|
151
|
+
const errorHandler = vi.fn();
|
|
152
|
+
|
|
153
|
+
bleService.on('error', errorHandler);
|
|
154
|
+
bleService.off('error', errorHandler);
|
|
155
|
+
bleService.emit('error', new Error('test error'));
|
|
156
|
+
|
|
157
|
+
expect(errorHandler).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('initialize()', () => {
|
|
162
|
+
it('should resolve true when BLE state is PoweredOn', async () => {
|
|
163
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
164
|
+
// Defer the immediate callback to next tick to avoid TDZ
|
|
165
|
+
if (immediate) {
|
|
166
|
+
Promise.resolve().then(() => callback(State.PoweredOn));
|
|
167
|
+
}
|
|
168
|
+
return { remove: vi.fn() };
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = await bleService.initialize();
|
|
172
|
+
expect(result).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should resolve false when BLE state is PoweredOff', async () => {
|
|
176
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
177
|
+
if (immediate) {
|
|
178
|
+
Promise.resolve().then(() => callback(State.PoweredOff));
|
|
179
|
+
}
|
|
180
|
+
return { remove: vi.fn() };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const result = await bleService.initialize();
|
|
184
|
+
expect(result).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should resolve false when BLE state is Unauthorized', async () => {
|
|
188
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
189
|
+
if (immediate) {
|
|
190
|
+
Promise.resolve().then(() => callback(State.Unauthorized));
|
|
191
|
+
}
|
|
192
|
+
return { remove: vi.fn() };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await bleService.initialize();
|
|
196
|
+
expect(result).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should timeout after 5 seconds and resolve false', async () => {
|
|
200
|
+
const removeSubscription = vi.fn();
|
|
201
|
+
mockManager.onStateChange.mockImplementation(() => {
|
|
202
|
+
// Never call the callback - simulate hanging
|
|
203
|
+
return { remove: removeSubscription };
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const initPromise = bleService.initialize();
|
|
207
|
+
|
|
208
|
+
// Fast-forward 5 seconds
|
|
209
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
210
|
+
|
|
211
|
+
const result = await initPromise;
|
|
212
|
+
expect(result).toBe(false);
|
|
213
|
+
expect(removeSubscription).toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should clear timeout when state resolves before timeout', async () => {
|
|
217
|
+
const removeSubscription = vi.fn();
|
|
218
|
+
mockManager.onStateChange.mockImplementation((callback: Function) => {
|
|
219
|
+
// Resolve after 1 second
|
|
220
|
+
setTimeout(() => callback(State.PoweredOn), 1000);
|
|
221
|
+
return { remove: removeSubscription };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const initPromise = bleService.initialize();
|
|
225
|
+
|
|
226
|
+
// Fast-forward 1 second (before 5s timeout)
|
|
227
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
228
|
+
|
|
229
|
+
const result = await initPromise;
|
|
230
|
+
expect(result).toBe(true);
|
|
231
|
+
expect(removeSubscription).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('startDiscovery()', () => {
|
|
236
|
+
it('should call initialize() before starting scan', async () => {
|
|
237
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
238
|
+
if (immediate) {
|
|
239
|
+
Promise.resolve().then(() => callback(State.PoweredOn));
|
|
240
|
+
}
|
|
241
|
+
return { remove: vi.fn() };
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await bleService.startDiscovery('testrpi', 'testkey');
|
|
245
|
+
|
|
246
|
+
expect(mockManager.onStateChange).toHaveBeenCalled();
|
|
247
|
+
expect(mockManager.startDeviceScan).toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should throw error if BLE is not available', async () => {
|
|
251
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
252
|
+
if (immediate) {
|
|
253
|
+
Promise.resolve().then(() => callback(State.PoweredOff));
|
|
254
|
+
}
|
|
255
|
+
return { remove: vi.fn() };
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await expect(bleService.startDiscovery('testrpi', 'testkey'))
|
|
259
|
+
.rejects.toThrow('Bluetooth is not available or not enabled');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should throw error if BLE times out', async () => {
|
|
263
|
+
mockManager.onStateChange.mockImplementation(() => {
|
|
264
|
+
// Never resolve - simulate timeout
|
|
265
|
+
return { remove: vi.fn() };
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const discoveryPromise = bleService.startDiscovery('testrpi', 'testkey');
|
|
269
|
+
|
|
270
|
+
// Prevent unhandled rejection by attaching catch handler immediately
|
|
271
|
+
const caughtPromise = discoveryPromise.catch(e => e);
|
|
272
|
+
|
|
273
|
+
// Fast-forward past timeout
|
|
274
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
275
|
+
|
|
276
|
+
// Verify the error was caught
|
|
277
|
+
const result = await caughtPromise;
|
|
278
|
+
expect(result).toBeInstanceOf(Error);
|
|
279
|
+
expect(result.message).toBe('Bluetooth is not available or not enabled');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should not start scan if already scanning', async () => {
|
|
283
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
284
|
+
if (immediate) {
|
|
285
|
+
Promise.resolve().then(() => callback(State.PoweredOn));
|
|
286
|
+
}
|
|
287
|
+
return { remove: vi.fn() };
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await bleService.startDiscovery('testrpi', 'testkey');
|
|
291
|
+
await bleService.startDiscovery('testrpi2', 'testkey2');
|
|
292
|
+
|
|
293
|
+
// startDeviceScan should only be called once
|
|
294
|
+
expect(mockManager.startDeviceScan).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('scan error emission', () => {
|
|
299
|
+
it('should emit error event when scan encounters error', async () => {
|
|
300
|
+
const errorHandler = vi.fn();
|
|
301
|
+
bleService.on('error', errorHandler);
|
|
302
|
+
|
|
303
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
304
|
+
if (immediate) {
|
|
305
|
+
Promise.resolve().then(() => callback(State.PoweredOn));
|
|
306
|
+
}
|
|
307
|
+
return { remove: vi.fn() };
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Capture the scan callback when startDeviceScan is called
|
|
311
|
+
mockManager.startDeviceScan.mockImplementation(
|
|
312
|
+
(uuids: string[], options: any, callback: Function) => {
|
|
313
|
+
// Simulate an error occurring during scan
|
|
314
|
+
const mockError = { message: 'Unknown error occurred', reason: 'bug' };
|
|
315
|
+
callback(mockError, null);
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
await bleService.startDiscovery('testrpi', 'testkey');
|
|
320
|
+
|
|
321
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
322
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({ message: 'Unknown error occurred' })
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should continue emitting errors for subsequent scan errors', async () => {
|
|
328
|
+
const errorHandler = vi.fn();
|
|
329
|
+
bleService.on('error', errorHandler);
|
|
330
|
+
|
|
331
|
+
mockManager.onStateChange.mockImplementation((callback: Function, immediate: boolean) => {
|
|
332
|
+
if (immediate) {
|
|
333
|
+
Promise.resolve().then(() => callback(State.PoweredOn));
|
|
334
|
+
}
|
|
335
|
+
return { remove: vi.fn() };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
let scanCallback: Function;
|
|
339
|
+
mockManager.startDeviceScan.mockImplementation(
|
|
340
|
+
(uuids: string[], options: any, callback: Function) => {
|
|
341
|
+
scanCallback = callback;
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
await bleService.startDiscovery('testrpi', 'testkey');
|
|
346
|
+
|
|
347
|
+
// Simulate multiple errors
|
|
348
|
+
scanCallback!({ message: 'Error 1' }, null);
|
|
349
|
+
scanCallback!({ message: 'Error 2' }, null);
|
|
350
|
+
scanCallback!({ message: 'Error 3' }, null);
|
|
351
|
+
|
|
352
|
+
expect(errorHandler).toHaveBeenCalledTimes(3);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
@@ -66,6 +66,10 @@ vi.mock('../src/ble', () => {
|
|
|
66
66
|
onNearbyUsersChanged = vi.fn().mockReturnValue(() => { });
|
|
67
67
|
pairWithUser = vi.fn().mockResolvedValue({ success: true });
|
|
68
68
|
unpairUser = vi.fn().mockResolvedValue(undefined);
|
|
69
|
+
// EventEmitter methods (BleService now extends EventEmitter)
|
|
70
|
+
on = vi.fn();
|
|
71
|
+
off = vi.fn();
|
|
72
|
+
emit = vi.fn();
|
|
69
73
|
},
|
|
70
74
|
};
|
|
71
75
|
});
|
package/dist/index.js
CHANGED
|
@@ -73,7 +73,7 @@ var IdentityManager = class {
|
|
|
73
73
|
try {
|
|
74
74
|
let key = await this.keyStorage.getKey();
|
|
75
75
|
if (!key) {
|
|
76
|
-
key = (0, import_react_native_quick_crypto.
|
|
76
|
+
key = (0, import_react_native_quick_crypto.randomBytes)(32).toString("hex");
|
|
77
77
|
await this.keyStorage.setKey(key);
|
|
78
78
|
}
|
|
79
79
|
this.masterKey = key;
|
|
@@ -370,6 +370,7 @@ var MatcherService = class extends import_eventemitter3.EventEmitter {
|
|
|
370
370
|
|
|
371
371
|
// src/ble.ts
|
|
372
372
|
var import_react_native_ble_plx = require("react-native-ble-plx");
|
|
373
|
+
var import_eventemitter32 = require("eventemitter3");
|
|
373
374
|
var VAILIX_SERVICE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
374
375
|
var RPI_OUT_CHAR_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567891";
|
|
375
376
|
var RPI_IN_CHAR_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567892";
|
|
@@ -391,7 +392,7 @@ function extractRpiPrefix(device, serviceUUID) {
|
|
|
391
392
|
}
|
|
392
393
|
return null;
|
|
393
394
|
}
|
|
394
|
-
var BleService = class {
|
|
395
|
+
var BleService = class extends import_eventemitter32.EventEmitter {
|
|
395
396
|
manager;
|
|
396
397
|
isScanning = false;
|
|
397
398
|
nearbyUsers = /* @__PURE__ */ new Map();
|
|
@@ -411,6 +412,7 @@ var BleService = class {
|
|
|
411
412
|
// Storage reference for persisting pairs
|
|
412
413
|
storage;
|
|
413
414
|
constructor(config = {}) {
|
|
415
|
+
super();
|
|
414
416
|
this.manager = new import_react_native_ble_plx.BleManager();
|
|
415
417
|
this.discoveryTimeoutMs = config.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
416
418
|
this.proximityThreshold = config.proximityThreshold ?? DEFAULT_PROXIMITY_THRESHOLD;
|
|
@@ -427,12 +429,19 @@ var BleService = class {
|
|
|
427
429
|
* Check if BLE is available and enabled
|
|
428
430
|
*/
|
|
429
431
|
async initialize() {
|
|
432
|
+
const INIT_TIMEOUT_MS = 5e3;
|
|
430
433
|
return new Promise((resolve) => {
|
|
434
|
+
const timeout = setTimeout(() => {
|
|
435
|
+
subscription.remove();
|
|
436
|
+
resolve(false);
|
|
437
|
+
}, INIT_TIMEOUT_MS);
|
|
431
438
|
const subscription = this.manager.onStateChange((state) => {
|
|
432
439
|
if (state === import_react_native_ble_plx.State.PoweredOn) {
|
|
440
|
+
clearTimeout(timeout);
|
|
433
441
|
subscription.remove();
|
|
434
442
|
resolve(true);
|
|
435
443
|
} else if (state === import_react_native_ble_plx.State.PoweredOff || state === import_react_native_ble_plx.State.Unauthorized) {
|
|
444
|
+
clearTimeout(timeout);
|
|
436
445
|
subscription.remove();
|
|
437
446
|
resolve(false);
|
|
438
447
|
}
|
|
@@ -458,6 +467,10 @@ var BleService = class {
|
|
|
458
467
|
*/
|
|
459
468
|
async startDiscovery(myRpi, myMetadataKey) {
|
|
460
469
|
if (this.isScanning) return;
|
|
470
|
+
const isReady = await this.initialize();
|
|
471
|
+
if (!isReady) {
|
|
472
|
+
throw new Error("Bluetooth is not available or not enabled");
|
|
473
|
+
}
|
|
461
474
|
this.myRpi = myRpi;
|
|
462
475
|
this.myMetadataKey = myMetadataKey;
|
|
463
476
|
this.isScanning = true;
|
|
@@ -559,6 +572,7 @@ var BleService = class {
|
|
|
559
572
|
(error, device) => {
|
|
560
573
|
if (error) {
|
|
561
574
|
console.warn("BLE scan error:", error);
|
|
575
|
+
this.emit("error", error);
|
|
562
576
|
return;
|
|
563
577
|
}
|
|
564
578
|
if (device) {
|
|
@@ -901,6 +915,9 @@ var VailixSDK = class _VailixSDK {
|
|
|
901
915
|
serviceUUID: config.serviceUUID
|
|
902
916
|
});
|
|
903
917
|
ble.setStorage(storage);
|
|
918
|
+
ble.on("error", (error) => {
|
|
919
|
+
matcher.emit("error", error);
|
|
920
|
+
});
|
|
904
921
|
await storage.cleanupOldScans();
|
|
905
922
|
return new _VailixSDK(
|
|
906
923
|
identity,
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createCipheriv, randomBytes } from "react-native-quick-crypto";
|
|
2
|
+
import { createCipheriv, randomBytes as randomBytes2 } from "react-native-quick-crypto";
|
|
3
3
|
|
|
4
4
|
// src/identity.ts
|
|
5
|
-
import { createHmac,
|
|
5
|
+
import { createHmac, randomBytes } from "react-native-quick-crypto";
|
|
6
6
|
import * as SecureStore from "expo-secure-store";
|
|
7
7
|
|
|
8
8
|
// src/utils.ts
|
|
@@ -37,7 +37,7 @@ var IdentityManager = class {
|
|
|
37
37
|
try {
|
|
38
38
|
let key = await this.keyStorage.getKey();
|
|
39
39
|
if (!key) {
|
|
40
|
-
key =
|
|
40
|
+
key = randomBytes(32).toString("hex");
|
|
41
41
|
await this.keyStorage.setKey(key);
|
|
42
42
|
}
|
|
43
43
|
this.masterKey = key;
|
|
@@ -89,7 +89,7 @@ var IdentityManager = class {
|
|
|
89
89
|
// src/storage.ts
|
|
90
90
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
|
91
91
|
import { lt, gt, inArray } from "drizzle-orm";
|
|
92
|
-
import { randomUUID
|
|
92
|
+
import { randomUUID } from "react-native-quick-crypto";
|
|
93
93
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
94
94
|
var SCAN_HISTORY_KEY = "vailix_scan_history";
|
|
95
95
|
var scannedEvents = sqliteTable("scanned_events", {
|
|
@@ -123,7 +123,7 @@ var StorageService = class _StorageService {
|
|
|
123
123
|
return Date.now() - lastScan >= this.rescanIntervalMs;
|
|
124
124
|
}
|
|
125
125
|
async logScan(rpi, metadataKey, timestamp) {
|
|
126
|
-
await this.db.insert(scannedEvents).values({ id:
|
|
126
|
+
await this.db.insert(scannedEvents).values({ id: randomUUID(), rpi, metadataKey, timestamp });
|
|
127
127
|
this.lastScanByRpi.set(rpi, Date.now());
|
|
128
128
|
if (this.lastScanByRpi.size > _StorageService.MAX_SCAN_HISTORY_SIZE) {
|
|
129
129
|
const entries2 = Array.from(this.lastScanByRpi.entries());
|
|
@@ -334,6 +334,7 @@ var MatcherService = class extends EventEmitter {
|
|
|
334
334
|
|
|
335
335
|
// src/ble.ts
|
|
336
336
|
import { BleManager, State } from "react-native-ble-plx";
|
|
337
|
+
import { EventEmitter as EventEmitter2 } from "eventemitter3";
|
|
337
338
|
var VAILIX_SERVICE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
338
339
|
var RPI_OUT_CHAR_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567891";
|
|
339
340
|
var RPI_IN_CHAR_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567892";
|
|
@@ -355,7 +356,7 @@ function extractRpiPrefix(device, serviceUUID) {
|
|
|
355
356
|
}
|
|
356
357
|
return null;
|
|
357
358
|
}
|
|
358
|
-
var BleService = class {
|
|
359
|
+
var BleService = class extends EventEmitter2 {
|
|
359
360
|
manager;
|
|
360
361
|
isScanning = false;
|
|
361
362
|
nearbyUsers = /* @__PURE__ */ new Map();
|
|
@@ -375,6 +376,7 @@ var BleService = class {
|
|
|
375
376
|
// Storage reference for persisting pairs
|
|
376
377
|
storage;
|
|
377
378
|
constructor(config = {}) {
|
|
379
|
+
super();
|
|
378
380
|
this.manager = new BleManager();
|
|
379
381
|
this.discoveryTimeoutMs = config.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
380
382
|
this.proximityThreshold = config.proximityThreshold ?? DEFAULT_PROXIMITY_THRESHOLD;
|
|
@@ -391,12 +393,19 @@ var BleService = class {
|
|
|
391
393
|
* Check if BLE is available and enabled
|
|
392
394
|
*/
|
|
393
395
|
async initialize() {
|
|
396
|
+
const INIT_TIMEOUT_MS = 5e3;
|
|
394
397
|
return new Promise((resolve) => {
|
|
398
|
+
const timeout = setTimeout(() => {
|
|
399
|
+
subscription.remove();
|
|
400
|
+
resolve(false);
|
|
401
|
+
}, INIT_TIMEOUT_MS);
|
|
395
402
|
const subscription = this.manager.onStateChange((state) => {
|
|
396
403
|
if (state === State.PoweredOn) {
|
|
404
|
+
clearTimeout(timeout);
|
|
397
405
|
subscription.remove();
|
|
398
406
|
resolve(true);
|
|
399
407
|
} else if (state === State.PoweredOff || state === State.Unauthorized) {
|
|
408
|
+
clearTimeout(timeout);
|
|
400
409
|
subscription.remove();
|
|
401
410
|
resolve(false);
|
|
402
411
|
}
|
|
@@ -422,6 +431,10 @@ var BleService = class {
|
|
|
422
431
|
*/
|
|
423
432
|
async startDiscovery(myRpi, myMetadataKey) {
|
|
424
433
|
if (this.isScanning) return;
|
|
434
|
+
const isReady = await this.initialize();
|
|
435
|
+
if (!isReady) {
|
|
436
|
+
throw new Error("Bluetooth is not available or not enabled");
|
|
437
|
+
}
|
|
425
438
|
this.myRpi = myRpi;
|
|
426
439
|
this.myMetadataKey = myMetadataKey;
|
|
427
440
|
this.isScanning = true;
|
|
@@ -523,6 +536,7 @@ var BleService = class {
|
|
|
523
536
|
(error, device) => {
|
|
524
537
|
if (error) {
|
|
525
538
|
console.warn("BLE scan error:", error);
|
|
539
|
+
this.emit("error", error);
|
|
526
540
|
return;
|
|
527
541
|
}
|
|
528
542
|
if (device) {
|
|
@@ -865,6 +879,9 @@ var VailixSDK = class _VailixSDK {
|
|
|
865
879
|
serviceUUID: config.serviceUUID
|
|
866
880
|
});
|
|
867
881
|
ble.setStorage(storage);
|
|
882
|
+
ble.on("error", (error) => {
|
|
883
|
+
matcher.emit("error", error);
|
|
884
|
+
});
|
|
868
885
|
await storage.cleanupOldScans();
|
|
869
886
|
return new _VailixSDK(
|
|
870
887
|
identity,
|
|
@@ -1055,7 +1072,7 @@ var VailixSDK = class _VailixSDK {
|
|
|
1055
1072
|
throw new Error(`Metadata exceeds maximum size of ${_VailixSDK.MAX_METADATA_SIZE} bytes`);
|
|
1056
1073
|
}
|
|
1057
1074
|
const key = Buffer.from(keyHex, "hex");
|
|
1058
|
-
const iv =
|
|
1075
|
+
const iv = randomBytes2(12);
|
|
1059
1076
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
1060
1077
|
let encrypted = cipher.update(jsonStr, "utf8", "base64");
|
|
1061
1078
|
encrypted += cipher.final("base64");
|
package/package.json
CHANGED
package/src/ble.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BleManager, Device, State, BleError } from 'react-native-ble-plx';
|
|
2
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
3
|
import type { NearbyUser, PairResult } from './types';
|
|
3
4
|
import type { StorageService } from './storage';
|
|
4
5
|
|
|
@@ -92,7 +93,7 @@ function extractRpiPrefix(device: Device, serviceUUID: string): string | null {
|
|
|
92
93
|
// BleService Class
|
|
93
94
|
// ============================================================================
|
|
94
95
|
|
|
95
|
-
export class BleService {
|
|
96
|
+
export class BleService extends EventEmitter {
|
|
96
97
|
private manager: BleManager;
|
|
97
98
|
private isScanning: boolean = false;
|
|
98
99
|
private nearbyUsers: Map<string, InternalNearbyUser> = new Map();
|
|
@@ -118,6 +119,7 @@ export class BleService {
|
|
|
118
119
|
private storage?: StorageService;
|
|
119
120
|
|
|
120
121
|
constructor(config: BleServiceConfig = {}) {
|
|
122
|
+
super(); // Initialize EventEmitter
|
|
121
123
|
this.manager = new BleManager();
|
|
122
124
|
this.discoveryTimeoutMs = config.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
123
125
|
this.proximityThreshold = config.proximityThreshold ?? DEFAULT_PROXIMITY_THRESHOLD;
|
|
@@ -136,12 +138,21 @@ export class BleService {
|
|
|
136
138
|
* Check if BLE is available and enabled
|
|
137
139
|
*/
|
|
138
140
|
async initialize(): Promise<boolean> {
|
|
141
|
+
const INIT_TIMEOUT_MS = 5000; // 5 second timeout
|
|
142
|
+
|
|
139
143
|
return new Promise((resolve) => {
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
subscription.remove();
|
|
146
|
+
resolve(false); // Timed out waiting for BLE state
|
|
147
|
+
}, INIT_TIMEOUT_MS);
|
|
148
|
+
|
|
140
149
|
const subscription = this.manager.onStateChange((state: typeof State[keyof typeof State]) => {
|
|
141
150
|
if (state === State.PoweredOn) {
|
|
151
|
+
clearTimeout(timeout);
|
|
142
152
|
subscription.remove();
|
|
143
153
|
resolve(true);
|
|
144
154
|
} else if (state === State.PoweredOff || state === State.Unauthorized) {
|
|
155
|
+
clearTimeout(timeout);
|
|
145
156
|
subscription.remove();
|
|
146
157
|
resolve(false);
|
|
147
158
|
}
|
|
@@ -170,6 +181,12 @@ export class BleService {
|
|
|
170
181
|
async startDiscovery(myRpi: string, myMetadataKey: string): Promise<void> {
|
|
171
182
|
if (this.isScanning) return;
|
|
172
183
|
|
|
184
|
+
// Ensure BLE is powered on before scanning
|
|
185
|
+
const isReady = await this.initialize();
|
|
186
|
+
if (!isReady) {
|
|
187
|
+
throw new Error('Bluetooth is not available or not enabled');
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
this.myRpi = myRpi;
|
|
174
191
|
this.myMetadataKey = myMetadataKey;
|
|
175
192
|
this.isScanning = true;
|
|
@@ -299,6 +316,7 @@ export class BleService {
|
|
|
299
316
|
(error: BleError | null, device: Device | null) => {
|
|
300
317
|
if (error) {
|
|
301
318
|
console.warn('BLE scan error:', error);
|
|
319
|
+
this.emit('error', error); // Propagate to listeners
|
|
302
320
|
return;
|
|
303
321
|
}
|
|
304
322
|
if (device) {
|
package/src/identity.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac,
|
|
1
|
+
import { createHmac, randomBytes } from 'react-native-quick-crypto';
|
|
2
2
|
import * as SecureStore from 'expo-secure-store';
|
|
3
3
|
import type { KeyStorage } from './types';
|
|
4
4
|
import { generateDisplayName } from './utils';
|
|
@@ -30,7 +30,7 @@ export class IdentityManager {
|
|
|
30
30
|
try {
|
|
31
31
|
let key = await this.keyStorage.getKey();
|
|
32
32
|
if (!key) {
|
|
33
|
-
key =
|
|
33
|
+
key = randomBytes(32).toString('hex');
|
|
34
34
|
await this.keyStorage.setKey(key);
|
|
35
35
|
}
|
|
36
36
|
this.masterKey = key;
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,12 @@ export class VailixSDK {
|
|
|
122
122
|
});
|
|
123
123
|
ble.setStorage(storage);
|
|
124
124
|
|
|
125
|
+
// Forward BLE errors to SDK error stream
|
|
126
|
+
// Consumers receive via sdk.onError()
|
|
127
|
+
ble.on('error', (error) => {
|
|
128
|
+
matcher.emit('error', error);
|
|
129
|
+
});
|
|
130
|
+
|
|
125
131
|
// Cleanup old scans on init
|
|
126
132
|
await storage.cleanupOldScans();
|
|
127
133
|
|