@onekeyfe/hd-transport-electron 1.2.8-alpha.5 → 1.2.8-alpha.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.
@@ -55,13 +55,13 @@ function invokeNobleBleIpc(request) {
55
55
 
56
56
  function initNobleBleSupport(webContents) {
57
57
  return __awaiter(this, void 0, void 0, function* () {
58
- const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-804f0a41.js'); });
58
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d0991fa0.js'); });
59
59
  setupNobleBleHandlers(webContents);
60
60
  });
61
61
  }
62
62
  function disposeNobleBleSupport(releaseNoble) {
63
63
  return __awaiter(this, void 0, void 0, function* () {
64
- const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-804f0a41.js'); });
64
+ const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d0991fa0.js'); });
65
65
  yield handler.disposeNobleBleSupport(releaseNoble);
66
66
  });
67
67
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('./index-2231e562.js');
5
+ var index = require('./index-e2e3d2ba.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-2231e562.js');
3
+ var index = require('./index-e2e3d2ba.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -123,6 +123,13 @@ function assertBleActive() {
123
123
  }
124
124
  function createNobleBleConnectionError(error, messagePrefix = '') {
125
125
  const errorMessage = error.message;
126
+ const isInvalidMacOsBond = error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
127
+ if (isInvalidMacOsBond) {
128
+ const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
129
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, `${hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`, {
130
+ nativeErrorMessage,
131
+ });
132
+ }
126
133
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `${messagePrefix}${errorMessage}`);
127
134
  }
128
135
  function createNobleBleIpcErrorResponse(error) {
@@ -376,18 +383,8 @@ function updateBluetoothState(state) {
376
383
  function initializeNoble() {
377
384
  return index.__awaiter(this, void 0, void 0, function* () {
378
385
  assertBleActive();
379
- if (noble) {
380
- if (noble.state === 'poweredOff') {
381
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
382
- }
383
- if (noble.state === 'unsupported') {
384
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
385
- }
386
- if (noble.state === 'unauthorized') {
387
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError);
388
- }
386
+ if (noble)
389
387
  return;
390
- }
391
388
  try {
392
389
  noble = require('@stoprocent/noble');
393
390
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
@@ -428,7 +425,7 @@ function initializeNoble() {
428
425
  }
429
426
  else if (state === 'unauthorized') {
430
427
  cleanup();
431
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError));
428
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
432
429
  }
433
430
  };
434
431
  const cancel = () => {
@@ -437,7 +434,6 @@ function initializeNoble() {
437
434
  };
438
435
  pendingCancellations.add(cancel);
439
436
  noble.on('stateChange', onStateChange);
440
- onStateChange(noble.state);
441
437
  });
442
438
  assertBleActive();
443
439
  if (!persistentDiscoverListener) {
@@ -450,7 +446,7 @@ function initializeNoble() {
450
446
  }
451
447
  catch (error) {
452
448
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to initialize Noble:', error);
453
- bluetoothState.unsupported = (noble === null || noble === void 0 ? void 0 : noble.state) === 'unsupported';
449
+ bluetoothState.unsupported = true;
454
450
  bluetoothState.initialized = true;
455
451
  throw error;
456
452
  }
@@ -879,7 +875,9 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
879
875
  }
880
876
  function enumerateDevices(isWindowDestroyed) {
881
877
  return index.__awaiter(this, void 0, void 0, function* () {
882
- yield initializeNoble();
878
+ if (!noble) {
879
+ yield initializeNoble();
880
+ }
883
881
  if (!noble) {
884
882
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
885
883
  }
@@ -1178,7 +1176,8 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
1178
1176
  yield forceReconnectPeripheral(peripheral, deviceId);
1179
1177
  }
1180
1178
  catch (resetError) {
1181
- if ((resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1179
+ if (hdShared.isBleStaleBondHardwareError(resetError) ||
1180
+ (resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1182
1181
  throw resetError;
1183
1182
  }
1184
1183
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
@@ -1276,7 +1275,6 @@ function tryDirectConnectById(deviceId) {
1276
1275
  function connectDevice(deviceId, webContents) {
1277
1276
  var _a, _b, _c;
1278
1277
  return index.__awaiter(this, void 0, void 0, function* () {
1279
- yield initializeNoble();
1280
1278
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
1281
1279
  deviceId,
1282
1280
  hasDiscovered: discoveredDevices.has(deviceId),
@@ -1290,6 +1288,9 @@ function connectDevice(deviceId, webContents) {
1290
1288
  peripheral = undefined;
1291
1289
  }
1292
1290
  if (!peripheral) {
1291
+ if (!noble) {
1292
+ yield initializeNoble();
1293
+ }
1293
1294
  if (!noble) {
1294
1295
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
1295
1296
  }
@@ -1312,17 +1313,29 @@ function connectDevice(deviceId, webContents) {
1312
1313
  return undefined;
1313
1314
  }
1314
1315
  });
1316
+ let staleBondError;
1315
1317
  const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
1316
- const found = yield tryDirectConnectById(deviceId);
1317
- if (found) {
1318
- discoveredDevices.set(deviceId, found);
1318
+ try {
1319
+ const found = yield tryDirectConnectById(deviceId);
1320
+ if (found) {
1321
+ discoveredDevices.set(deviceId, found);
1322
+ }
1323
+ return found;
1324
+ }
1325
+ catch (error) {
1326
+ if (error.errorCode !== hdShared.HardwareErrorCode.BleBondInvalid) {
1327
+ throw error;
1328
+ }
1329
+ staleBondError = error;
1330
+ return undefined;
1319
1331
  }
1320
- return found;
1321
1332
  });
1322
1333
  peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
1323
1334
  if (!peripheral) {
1324
1335
  peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1325
1336
  }
1337
+ if (!peripheral && staleBondError)
1338
+ throw staleBondError;
1326
1339
  }
1327
1340
  assertBleActive();
1328
1341
  if (!peripheral) {
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAG3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAszDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkOpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAgB3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA2zDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkOpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-electron",
3
- "version": "1.2.8-alpha.5",
3
+ "version": "1.2.8-alpha.6",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  "electron-log": ">=4.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-core": "1.2.8-alpha.5",
29
- "@onekeyfe/hd-shared": "1.2.8-alpha.5",
30
- "@onekeyfe/hd-transport": "1.2.8-alpha.5",
28
+ "@onekeyfe/hd-core": "1.2.8-alpha.6",
29
+ "@onekeyfe/hd-shared": "1.2.8-alpha.6",
30
+ "@onekeyfe/hd-transport": "1.2.8-alpha.6",
31
31
  "@stoprocent/noble": "2.3.16",
32
32
  "p-retry": "^4.6.2"
33
33
  },
@@ -36,5 +36,5 @@
36
36
  "electron": "^25.0.0",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
- "gitHead": "c1f24dc0a59d0c02fe05594a9c4ae7652ec9cc32"
39
+ "gitHead": "eba2e870aaf79093ba41c67eb616f7239527a84b"
40
40
  }
@@ -31,17 +31,22 @@ describe('Electron Noble BLE device discovery', () => {
31
31
  expect(NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS).toBe(10_000);
32
32
  });
33
33
 
34
- test('maps native macOS connection failures to the generic connection error', async () => {
34
+ test('maps structured macOS stale pairing failures without parsing localized text', async () => {
35
35
  const { createNobleBleConnectionError } = await import('../noble-ble-handler');
36
36
 
37
- expect(
38
- createNobleBleConnectionError(
39
- Object.assign(new Error('Peer removed pairing information on the device side'), {
40
- nativeErrorCode: 14,
41
- nativeErrorDomain: 'CBErrorDomain',
42
- })
43
- )
44
- ).toMatchObject({ errorCode: HardwareErrorCode.BleConnectedError });
37
+ const staleBondError = createNobleBleConnectionError(
38
+ Object.assign(new Error('Peer removed pairing information on the device side'), {
39
+ nativeErrorCode: 14,
40
+ nativeErrorDomain: 'CBErrorDomain',
41
+ })
42
+ );
43
+ expect(staleBondError).toMatchObject({
44
+ errorCode: HardwareErrorCode.BleBondInvalid,
45
+ params: {
46
+ nativeErrorMessage: 'Peer removed pairing information on the device side',
47
+ },
48
+ });
49
+ expect(staleBondError.message).toContain('Peer removed pairing information on the device side');
45
50
  expect(createNobleBleConnectionError(new Error('Encryption is insufficient'))).toMatchObject({
46
51
  errorCode: HardwareErrorCode.BleConnectedError,
47
52
  });
@@ -76,16 +81,18 @@ describe('Electron Noble BLE device discovery', () => {
76
81
  expect(
77
82
  createNobleBleIpcErrorResponse({
78
83
  name: 'HardwareError',
79
- message: 'Bluetooth connection failed',
80
- errorCode: HardwareErrorCode.BleConnectedError,
84
+ message: 'Bluetooth pairing information is no longer valid',
85
+ errorCode: HardwareErrorCode.BleBondInvalid,
86
+ params: { nativeErrorMessage: 'native message' },
81
87
  })
82
88
  ).toEqual({
83
89
  type: 'NobleBleIpcError',
84
90
  success: false,
85
91
  error: {
86
92
  name: 'HardwareError',
87
- message: 'Bluetooth connection failed',
88
- errorCode: HardwareErrorCode.BleConnectedError,
93
+ message: 'Bluetooth pairing information is no longer valid',
94
+ errorCode: HardwareErrorCode.BleBondInvalid,
95
+ params: { nativeErrorMessage: 'native message' },
89
96
  },
90
97
  });
91
98
 
@@ -128,14 +135,14 @@ describe('Electron Noble BLE device discovery', () => {
128
135
  success: false as const,
129
136
  error: {
130
137
  name: 'HardwareError',
131
- message: 'Bluetooth connection failed',
132
- errorCode: HardwareErrorCode.BleConnectedError,
138
+ message: 'Bluetooth pairing information is no longer valid',
139
+ errorCode: HardwareErrorCode.BleBondInvalid,
133
140
  },
134
141
  })
135
142
  )
136
143
  ).rejects.toMatchObject({
137
144
  name: 'HardwareError',
138
- errorCode: HardwareErrorCode.BleConnectedError,
145
+ errorCode: HardwareErrorCode.BleBondInvalid,
139
146
  });
140
147
 
141
148
  await expect(invokeNobleBleIpc(Promise.resolve('connected'))).resolves.toBe('connected');
@@ -345,6 +352,87 @@ describe('Electron Noble BLE device discovery', () => {
345
352
  }
346
353
  );
347
354
 
355
+ test.each([
356
+ { rediscovered: false, expectedCode: HardwareErrorCode.BleBondInvalid },
357
+ { rediscovered: true, expectedCode: HardwareErrorCode.BleConnectedError },
358
+ ])(
359
+ 'reports a macOS stale bond only when fallback scan misses (rediscovered=$rediscovered)',
360
+ async ({ rediscovered, expectedCode }) => {
361
+ jest.useFakeTimers({ doNotFake: ['performance'] });
362
+ const handlers = new Map<string, IpcHandler>();
363
+ const stalePeripheral = createPeripheral('device', 'Pro2 A1B2');
364
+ const freshPeripheral = Object.assign(
365
+ new EventEmitter(),
366
+ createPeripheral('device', 'Pro2 A1B2'),
367
+ {
368
+ connect: jest.fn((callback: (error?: Error) => void) => {
369
+ callback(new Error('fresh connection failed'));
370
+ }),
371
+ }
372
+ );
373
+ const staleBondError = Object.assign(new Error('Peer removed pairing information'), {
374
+ nativeErrorCode: 14,
375
+ nativeErrorDomain: 'CBErrorDomain',
376
+ });
377
+ let scanStarted: (() => void) | undefined;
378
+ const scanning = new Promise<void>(resolve => {
379
+ scanStarted = resolve;
380
+ });
381
+ const noble = Object.assign(new EventEmitter(), {
382
+ state: 'poweredOn',
383
+ startScanning: jest.fn((_services, _duplicates, callback) => {
384
+ callback?.();
385
+ if (rediscovered) noble.emit('discover', freshPeripheral);
386
+ scanStarted?.();
387
+ }),
388
+ stopScanning: jest.fn(callback => callback?.()),
389
+ connectAsync: jest.fn(() => Promise.reject(staleBondError)),
390
+ });
391
+ jest.doMock('@stoprocent/noble', () => noble);
392
+ jest.doMock('electron', () => ({
393
+ ipcMain: {
394
+ handle: (channel: string, handler: IpcHandler) => handlers.set(channel, handler),
395
+ removeHandler: (channel: string) => handlers.delete(channel),
396
+ },
397
+ }));
398
+ jest.doMock('electron-log', () => ({
399
+ info: jest.fn(),
400
+ debug: jest.fn(),
401
+ error: jest.fn(),
402
+ }));
403
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
404
+ setupNobleBleHandlers({ on: jest.fn(), send: jest.fn() } as unknown as WebContents);
405
+ const availability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK);
406
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
407
+ if (!availability || !connect) throw new Error('Noble handlers were not registered');
408
+ await availability();
409
+ noble.emit('discover', stalePeripheral);
410
+
411
+ const connecting = Promise.resolve(connect(undefined, stalePeripheral.id));
412
+ await scanning;
413
+ jest.advanceTimersByTime(1500);
414
+ await expect(connecting).resolves.toMatchObject({
415
+ success: false,
416
+ error: { errorCode: expectedCode },
417
+ });
418
+ if (!rediscovered) {
419
+ const secondScan = new Promise<void>(resolve => {
420
+ scanStarted = resolve;
421
+ });
422
+ const retry = Promise.resolve(connect(undefined, stalePeripheral.id));
423
+ await secondScan;
424
+ jest.advanceTimersByTime(1500);
425
+ await expect(retry).resolves.toMatchObject({
426
+ success: false,
427
+ error: { errorCode: HardwareErrorCode.BleBondInvalid },
428
+ });
429
+ }
430
+ expect(noble.connectAsync).toHaveBeenCalledTimes(rediscovered ? 1 : 2);
431
+ expect(noble.startScanning).toHaveBeenCalledTimes(rediscovered ? 1 : 2);
432
+ expect(freshPeripheral.connect).toHaveBeenCalledTimes(rediscovered ? 1 : 0);
433
+ }
434
+ );
435
+
348
436
  test('waits for a targeted scan to stop before connecting', async () => {
349
437
  jest.useFakeTimers({ doNotFake: ['performance'] });
350
438
 
@@ -918,59 +1006,6 @@ describe('Noble BLE process shutdown', () => {
918
1006
  expect(handlers.size).toBe(0);
919
1007
  });
920
1008
 
921
- test.each([
922
- ['poweredOff', HardwareErrorCode.BlePoweredOff],
923
- ['unauthorized', HardwareErrorCode.BleLocationError],
924
- ['unsupported', HardwareErrorCode.BleUnsupported],
925
- ])(
926
- 'preserves adapter state %s across repeated scans and connections',
927
- async (state, errorCode) => {
928
- jest.useFakeTimers({ doNotFake: ['performance'] });
929
- const { sdk, native, handlers } = await setup(state);
930
- for (let attempt = 0; attempt < 2; attempt += 1) {
931
- for (const channel of [
932
- EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE,
933
- EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT,
934
- ]) {
935
- await expect(handlers.get(channel)?.({}, 'device')).resolves.toMatchObject({
936
- success: false,
937
- error: { errorCode },
938
- });
939
- }
940
- }
941
- expect(native.startScanning).not.toHaveBeenCalled();
942
- expect(native.listenerCount('stateChange')).toBe(1);
943
- expect(jest.getTimerCount()).toBe(0);
944
- await sdk.disposeNobleBleSupport();
945
- }
946
- );
947
-
948
- test.each([
949
- ['poweredOff', HardwareErrorCode.BlePoweredOff],
950
- ['unauthorized', HardwareErrorCode.BleLocationError],
951
- ['unsupported', HardwareErrorCode.BleUnsupported],
952
- ])('checks adapter state %s again after initialization', async (state, errorCode) => {
953
- const { sdk, native, handlers } = await setup();
954
- const checkAvailability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK);
955
- await expect(checkAvailability?.({})).resolves.toMatchObject({ available: true });
956
- native.state = state;
957
- native.emit('stateChange', state);
958
- for (const channel of [
959
- EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE,
960
- EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT,
961
- ]) {
962
- await expect(handlers.get(channel)?.({}, 'device')).resolves.toMatchObject({
963
- success: false,
964
- error: { errorCode },
965
- });
966
- }
967
- expect(native.startScanning).not.toHaveBeenCalled();
968
- native.state = 'poweredOn';
969
- native.emit('stateChange', native.state);
970
- await expect(checkAvailability?.({})).resolves.toMatchObject({ available: true });
971
- await sdk.disposeNobleBleSupport();
972
- });
973
-
974
1009
  test('cancels an active scan, ignores its late callback and releases native once', async () => {
975
1010
  jest.useFakeTimers({ doNotFake: ['performance'] });
976
1011
  const { sdk, native, handlers } = await setup();
@@ -16,6 +16,7 @@ import {
16
16
  ONEKEY_WRITE_CHARACTERISTIC_UUID,
17
17
  createKnownBleUuidAliases,
18
18
  hasOnekeyCommunicationService,
19
+ isBleStaleBondHardwareError,
19
20
  isOnekeyBluetoothDevice,
20
21
  isPro2FamilyBleName,
21
22
  matchesKnownBleUuid,
@@ -73,6 +74,19 @@ type NobleBleNativeError = Error & {
73
74
 
74
75
  export function createNobleBleConnectionError(error: NobleBleNativeError, messagePrefix = '') {
75
76
  const errorMessage = error.message;
77
+ const isInvalidMacOsBond =
78
+ error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
79
+ if (isInvalidMacOsBond) {
80
+ const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
81
+ return ERRORS.TypedError(
82
+ HardwareErrorCode.BleBondInvalid,
83
+ `${HardwareErrorCodeMessage[HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`,
84
+ {
85
+ nativeErrorMessage,
86
+ }
87
+ );
88
+ }
89
+
76
90
  return ERRORS.TypedError(HardwareErrorCode.BleConnectedError, `${messagePrefix}${errorMessage}`);
77
91
  }
78
92
 
@@ -429,18 +443,7 @@ function updateBluetoothState(state: string): void {
429
443
  // Initialize Noble
430
444
  async function initializeNoble(): Promise<void> {
431
445
  assertBleActive();
432
- if (noble) {
433
- if (noble.state === 'poweredOff') {
434
- throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
435
- }
436
- if (noble.state === 'unsupported') {
437
- throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
438
- }
439
- if (noble.state === 'unauthorized') {
440
- throw ERRORS.TypedError(HardwareErrorCode.BleLocationError);
441
- }
442
- return;
443
- }
446
+ if (noble) return;
444
447
 
445
448
  try {
446
449
  // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
@@ -493,7 +496,7 @@ async function initializeNoble(): Promise<void> {
493
496
  reject(ERRORS.TypedError(HardwareErrorCode.BlePoweredOff));
494
497
  } else if (state === 'unauthorized') {
495
498
  cleanup();
496
- reject(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
499
+ reject(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
497
500
  }
498
501
  };
499
502
 
@@ -505,7 +508,6 @@ async function initializeNoble(): Promise<void> {
505
508
  };
506
509
  pendingCancellations.add(cancel);
507
510
  noble.on('stateChange', onStateChange);
508
- onStateChange(noble.state);
509
511
  });
510
512
 
511
513
  assertBleActive();
@@ -520,7 +522,7 @@ async function initializeNoble(): Promise<void> {
520
522
  logger?.info('[NobleBLE] Noble initialized successfully');
521
523
  } catch (error) {
522
524
  logger?.error('[NobleBLE] Failed to initialize Noble:', error);
523
- bluetoothState.unsupported = noble?.state === 'unsupported';
525
+ bluetoothState.unsupported = true;
524
526
  bluetoothState.initialized = true;
525
527
  throw error;
526
528
  }
@@ -1149,7 +1151,9 @@ async function performTargetedScan(
1149
1151
 
1150
1152
  // Enumerate devices
1151
1153
  async function enumerateDevices(isWindowDestroyed: () => boolean): Promise<DeviceInfo[]> {
1152
- await initializeNoble();
1154
+ if (!noble) {
1155
+ await initializeNoble();
1156
+ }
1153
1157
 
1154
1158
  if (!noble) {
1155
1159
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
@@ -1630,6 +1634,7 @@ async function setupConnectionAndDiscoverServices(
1630
1634
  await forceReconnectPeripheral(peripheral, deviceId);
1631
1635
  } catch (resetError) {
1632
1636
  if (
1637
+ isBleStaleBondHardwareError(resetError) ||
1633
1638
  (resetError as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceDisconnected
1634
1639
  ) {
1635
1640
  throw resetError;
@@ -1754,7 +1759,6 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
1754
1759
 
1755
1760
  // Connect to device - supports both discovered and direct connection modes
1756
1761
  async function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
1757
- await initializeNoble();
1758
1762
  logger?.info('[NobleBLE] Connect device request:', {
1759
1763
  deviceId,
1760
1764
  hasDiscovered: discoveredDevices.has(deviceId),
@@ -1772,6 +1776,11 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1772
1776
  }
1773
1777
 
1774
1778
  if (!peripheral) {
1779
+ // Initialize Noble if not already done
1780
+ if (!noble) {
1781
+ await initializeNoble();
1782
+ }
1783
+
1775
1784
  if (!noble) {
1776
1785
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
1777
1786
  }
@@ -1803,12 +1812,21 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1803
1812
  }
1804
1813
  };
1805
1814
 
1815
+ let staleBondError: Error | undefined;
1806
1816
  const connectById = async () => {
1807
- const found = await tryDirectConnectById(deviceId);
1808
- if (found) {
1809
- discoveredDevices.set(deviceId, found);
1817
+ try {
1818
+ const found = await tryDirectConnectById(deviceId);
1819
+ if (found) {
1820
+ discoveredDevices.set(deviceId, found);
1821
+ }
1822
+ return found;
1823
+ } catch (error) {
1824
+ if ((error as { errorCode?: number }).errorCode !== HardwareErrorCode.BleBondInvalid) {
1825
+ throw error;
1826
+ }
1827
+ staleBondError = error as Error;
1828
+ return undefined;
1810
1829
  }
1811
- return found;
1812
1830
  };
1813
1831
 
1814
1832
  peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
@@ -1817,6 +1835,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1817
1835
  // silent), or not reachable by id. Try the other one before giving up.
1818
1836
  peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
1819
1837
  }
1838
+ if (!peripheral && staleBondError) throw staleBondError;
1820
1839
  }
1821
1840
 
1822
1841
  assertBleActive();