@onekeyfe/hd-transport-web-device 1.2.8-alpha.10 → 1.2.8-alpha.101

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/dist/index.js CHANGED
@@ -51,11 +51,15 @@ const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
51
51
  const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
52
52
  const PACKET_IO_MAX_RETRIES = 3;
53
53
  const PACKET_IO_RETRY_DELAY = 300;
54
+ const USB_DEVICE_IN_USE = 'USB device is already in use';
54
55
  const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
55
56
  const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
56
57
  const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
57
58
  let activeWebUsbTransport;
58
59
  let usbDisconnectListenerAttached = false;
60
+ const identityProbeHandles = new WeakSet();
61
+ const openingHandles = new WeakSet();
62
+ const normalHandleOwners = new WeakMap();
59
63
  function registerActiveWebUsbTransport(instance, usb) {
60
64
  activeWebUsbTransport = instance;
61
65
  if (usbDisconnectListenerAttached)
@@ -72,17 +76,22 @@ function registerActiveWebUsbTransport(instance, usb) {
72
76
  });
73
77
  }
74
78
  class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
75
- constructor() {
79
+ constructor(identityProbe = false) {
76
80
  super({
77
81
  router: transport.PROTOCOL_V2_CHANNEL_USB,
78
82
  maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
79
83
  logPrefix: 'ProtocolV2 WebUSB',
80
84
  });
85
+ this.identityProbe = identityProbe;
81
86
  this.deviceProtocol = new Map();
82
87
  this.confirmedDeviceProtocols = new Map();
83
88
  this.deviceProtocolHints = new Map();
84
89
  this.staleProtocolPaths = new Set();
85
90
  this.acquiredPaths = new Set();
91
+ this.identityProbeDevices = new Map();
92
+ this.normalDevices = new Map();
93
+ this.pendingHandleOperations = new Set();
94
+ this.identityProbeCleanupPaths = new Set();
86
95
  this.probedDeviceObjects = new Map();
87
96
  this.deviceEndpoints = new Map();
88
97
  this.name = 'WebUsbTransport';
@@ -101,7 +110,104 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
101
110
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
102
111
  }
103
112
  this.usb = usb;
104
- registerActiveWebUsbTransport(this, usb);
113
+ if (!this.identityProbe)
114
+ registerActiveWebUsbTransport(this, usb);
115
+ }
116
+ stop() {
117
+ this.stopped = true;
118
+ this.acquiredPaths.clear();
119
+ if (activeWebUsbTransport === this)
120
+ activeWebUsbTransport = undefined;
121
+ if (!this.stopPromise) {
122
+ this.stopPromise = this.stopResources().catch(error => {
123
+ this.stopPromise = undefined;
124
+ throw error;
125
+ });
126
+ }
127
+ return this.stopPromise;
128
+ }
129
+ stopResources() {
130
+ var _a, _b, _c;
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ let cleanupError;
133
+ try {
134
+ yield this.disposeProtocolV2UsbLinks('WebUSB transport stopped');
135
+ }
136
+ catch (error) {
137
+ cleanupError = error;
138
+ }
139
+ yield Promise.allSettled([...this.pendingHandleOperations]);
140
+ const handles = this.identityProbe
141
+ ? [...this.identityProbeDevices].map(([path, device]) => [device, path])
142
+ : [...this.normalDevices];
143
+ for (const [device, path] of handles) {
144
+ if (!this.identityProbe && normalHandleOwners.get(device) !== this) {
145
+ this.normalDevices.delete(device);
146
+ }
147
+ else {
148
+ try {
149
+ if (device.opened) {
150
+ const iface = (_b = (_a = this.deviceEndpoints.get(path)) === null || _a === void 0 ? void 0 : _a.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
151
+ try {
152
+ yield device.releaseInterface(iface);
153
+ }
154
+ catch (error) {
155
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] stop releaseInterface failed:', error);
156
+ }
157
+ yield device.close();
158
+ if (device.opened)
159
+ throw new Error('WebUSB handle remains open after stop');
160
+ }
161
+ if (this.identityProbe) {
162
+ identityProbeHandles.delete(device);
163
+ this.identityProbeDevices.delete(path);
164
+ this.identityProbeCleanupPaths.delete(path);
165
+ }
166
+ else {
167
+ normalHandleOwners.delete(device);
168
+ this.normalDevices.delete(device);
169
+ }
170
+ }
171
+ catch (error) {
172
+ cleanupError !== null && cleanupError !== void 0 ? cleanupError : (cleanupError = error);
173
+ }
174
+ }
175
+ }
176
+ this.deviceList = [];
177
+ this.deviceProtocol.clear();
178
+ this.confirmedDeviceProtocols.clear();
179
+ this.deviceProtocolHints.clear();
180
+ this.staleProtocolPaths.clear();
181
+ this.probedDeviceObjects.clear();
182
+ if (!this.normalDevices.size && !this.identityProbeDevices.size) {
183
+ this.deviceEndpoints.clear();
184
+ }
185
+ if (cleanupError) {
186
+ throw cleanupError instanceof Error
187
+ ? cleanupError
188
+ : new Error(this.getErrorMessage(cleanupError));
189
+ }
190
+ });
191
+ }
192
+ assertRunning() {
193
+ if (this.stopped) {
194
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB transport is stopped');
195
+ }
196
+ }
197
+ runHandleOperation(operation) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ this.assertRunning();
200
+ const pending = Promise.resolve(operation());
201
+ this.pendingHandleOperations.add(pending);
202
+ try {
203
+ const result = yield pending;
204
+ this.assertRunning();
205
+ return result;
206
+ }
207
+ finally {
208
+ this.pendingHandleOperations.delete(pending);
209
+ }
210
+ });
105
211
  }
106
212
  emitDeviceDisconnect(path, device) {
107
213
  var _a, _b;
@@ -150,15 +256,36 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
150
256
  }
151
257
  enumerate() {
152
258
  return __awaiter(this, void 0, void 0, function* () {
259
+ for (const path of this.identityProbeCleanupPaths) {
260
+ try {
261
+ yield this.release(path);
262
+ }
263
+ catch (_a) {
264
+ }
265
+ }
153
266
  yield this.getConnectedDevices();
154
267
  return this.deviceList;
155
268
  });
156
269
  }
157
270
  getConnectedDevices() {
271
+ var _a;
158
272
  return __awaiter(this, void 0, void 0, function* () {
273
+ this.assertRunning();
159
274
  if (!this.usb)
160
275
  return [];
161
276
  const devices = yield this.usb.getDevices();
277
+ this.assertRunning();
278
+ for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
279
+ if (owner === null || owner === void 0 ? void 0 : owner.stopped) {
280
+ try {
281
+ yield owner.stop();
282
+ }
283
+ catch (error) {
284
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] stopped owner cleanup failed:', error);
285
+ }
286
+ }
287
+ }
288
+ this.assertRunning();
162
289
  const onekeyDevices = devices.filter(dev => {
163
290
  const isOneKey = hdShared.ONEKEY_WEBUSB_FILTER.some((desc) => dev.vendorId === desc.vendorId && dev.productId === desc.productId);
164
291
  return isOneKey && !hdShared.isKnownTrezorWebUsbDevice(dev);
@@ -185,8 +312,16 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
185
312
  acquire(input) {
186
313
  var _a, _b, _c, _d, _e, _f, _g;
187
314
  return __awaiter(this, void 0, void 0, function* () {
315
+ this.assertRunning();
188
316
  if (!input.path)
189
317
  return;
318
+ if (this.identityProbe) {
319
+ yield this.getConnectedDevices();
320
+ this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
321
+ }
322
+ else {
323
+ this.claimNormalHandle(input.path, yield this.findDevice(input.path));
324
+ }
190
325
  try {
191
326
  yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
192
327
  yield this.closeOpenDevice(input.path);
@@ -236,31 +371,44 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
236
371
  this.deviceProtocolHints.set(input.path, protocolHint);
237
372
  }
238
373
  const detectedProtocol = yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
374
+ this.assertRunning();
239
375
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
240
376
  const probedDevice = (_g = this.deviceList.find(d => d.path === input.path)) === null || _g === void 0 ? void 0 : _g.device;
241
377
  if (probedDevice) {
242
378
  this.probedDeviceObjects.set(input.path, probedDevice);
243
379
  }
244
380
  }
381
+ this.assertRunning();
245
382
  this.acquiredPaths.add(input.path);
246
383
  return yield Promise.resolve(input.path);
247
384
  }
248
385
  catch (e) {
249
386
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
250
387
  this.acquiredPaths.delete(input.path);
251
- yield this.closeOpenDevice(input.path);
388
+ try {
389
+ yield this.closeOpenDevice(input.path);
390
+ }
391
+ finally {
392
+ this.releaseIdentityProbeHandle(input.path);
393
+ this.releaseNormalHandles(input.path);
394
+ }
252
395
  throw e;
253
396
  }
254
397
  });
255
398
  }
256
399
  createProtocolMismatchError(expected) {
257
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
400
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
401
+ expectedProtocol: expected,
402
+ });
258
403
  }
259
404
  createProtocolProbeTimeoutError(expected, attempts) {
260
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInitializeFailed, `Protocol ${expected} probe timeout after ${attempts} attempts`);
405
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
406
+ expectedProtocol: expected,
407
+ attempts,
408
+ });
261
409
  }
262
410
  createProtocolDetectionError() {
263
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
411
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed);
264
412
  }
265
413
  detectProtocol(path, expectedProtocol, protocolHint) {
266
414
  var _a;
@@ -328,10 +476,15 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
328
476
  return __awaiter(this, void 0, void 0, function* () {
329
477
  const maxRetries = 5;
330
478
  for (let i = 0; i < maxRetries; i++) {
479
+ this.assertRunning();
331
480
  try {
332
481
  return yield this.connectToDevice(path, first);
333
482
  }
334
483
  catch (e) {
484
+ this.assertRunning();
485
+ if (e instanceof hdShared.HardwareError && e.errorCode === hdShared.HardwareErrorCode.BridgeNeedsPermission) {
486
+ throw e;
487
+ }
335
488
  if (i === maxRetries - 1) {
336
489
  throw e;
337
490
  }
@@ -371,69 +524,174 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
371
524
  connectToDevice(path, first) {
372
525
  var _a;
373
526
  return __awaiter(this, void 0, void 0, function* () {
527
+ this.assertRunning();
374
528
  let device = yield this.findDevice(path);
529
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
530
+ yield this.closeOpenDevice(path);
531
+ this.releaseIdentityProbeHandle(path);
532
+ this.claimIdentityProbeHandle(path, device);
533
+ }
534
+ if (!this.identityProbe)
535
+ this.claimNormalHandle(path, device);
536
+ this.assertHandleAvailable(device);
375
537
  if (!device.opened) {
376
- yield device.open();
538
+ openingHandles.add(device);
539
+ try {
540
+ yield this.runHandleOperation(() => device.open());
541
+ }
542
+ catch (error) {
543
+ return yield this.throwWebUsbIoError(path, 'open', error);
544
+ }
545
+ finally {
546
+ openingHandles.delete(device);
547
+ }
377
548
  }
378
549
  if (first || this.deviceProtocol.get(path) !== 'V1') {
379
550
  try {
380
- yield device.reset();
551
+ yield this.runHandleOperation(() => device.reset());
381
552
  }
382
553
  catch (error) {
554
+ this.assertRunning();
383
555
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
384
556
  }
385
557
  }
386
558
  yield this.getConnectedDevices();
387
559
  device = yield this.findDevice(path);
560
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
561
+ yield this.closeOpenDevice(path);
562
+ this.releaseIdentityProbeHandle(path);
563
+ this.claimIdentityProbeHandle(path, device);
564
+ }
565
+ if (!this.identityProbe)
566
+ this.claimNormalHandle(path, device);
567
+ this.assertHandleAvailable(device);
388
568
  if (!device.opened) {
389
- yield device.open();
569
+ openingHandles.add(device);
570
+ try {
571
+ yield this.runHandleOperation(() => device.open());
572
+ }
573
+ catch (error) {
574
+ return yield this.throwWebUsbIoError(path, 'open', error);
575
+ }
576
+ finally {
577
+ openingHandles.delete(device);
578
+ }
390
579
  }
391
580
  if (first ||
392
581
  !device.configuration ||
393
582
  device.configuration.configurationValue !== this.configurationId) {
394
- yield device.selectConfiguration(this.configurationId);
583
+ yield this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
395
584
  }
396
585
  const endpoints = this.discoverEndpoints(device);
397
586
  this.deviceEndpoints.set(path, endpoints);
398
- yield device.claimInterface(endpoints.interfaceNumber);
587
+ try {
588
+ yield this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
589
+ }
590
+ catch (error) {
591
+ return this.throwWebUsbIoError(path, 'claimInterface', error);
592
+ }
399
593
  yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
400
594
  yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
401
595
  });
402
596
  }
597
+ assertHandleAvailable(device) {
598
+ this.assertRunning();
599
+ const normalOwner = normalHandleOwners.get(device);
600
+ if ((normalOwner && normalOwner !== this) ||
601
+ (identityProbeHandles.has(device) &&
602
+ ![...this.identityProbeDevices.values()].includes(device))) {
603
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
604
+ }
605
+ }
606
+ claimNormalHandle(path, device) {
607
+ this.assertHandleAvailable(device);
608
+ normalHandleOwners.set(device, this);
609
+ this.normalDevices.set(device, path);
610
+ }
611
+ releaseNormalHandles(path) {
612
+ if (this.stopped)
613
+ return;
614
+ for (const [device, ownedPath] of this.normalDevices) {
615
+ if (ownedPath === path) {
616
+ normalHandleOwners.delete(device);
617
+ this.normalDevices.delete(device);
618
+ }
619
+ }
620
+ }
621
+ isHandleOwnershipError(error) {
622
+ return (error instanceof hdShared.HardwareError &&
623
+ error.errorCode === hdShared.HardwareErrorCode.RuntimeError &&
624
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE));
625
+ }
626
+ claimIdentityProbeHandle(path, device) {
627
+ this.assertRunning();
628
+ if (device.opened ||
629
+ openingHandles.has(device) ||
630
+ identityProbeHandles.has(device) ||
631
+ normalHandleOwners.has(device)) {
632
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
633
+ }
634
+ identityProbeHandles.add(device);
635
+ this.identityProbeDevices.set(path, device);
636
+ }
637
+ releaseIdentityProbeHandle(path) {
638
+ if (this.stopped)
639
+ return;
640
+ const device = this.identityProbeDevices.get(path);
641
+ if (device === null || device === void 0 ? void 0 : device.opened) {
642
+ this.identityProbeCleanupPaths.add(path);
643
+ return;
644
+ }
645
+ if (device)
646
+ identityProbeHandles.delete(device);
647
+ this.identityProbeDevices.delete(path);
648
+ this.identityProbeCleanupPaths.delete(path);
649
+ }
403
650
  closeOpenDevice(path) {
404
651
  var _a, _b, _c, _d;
405
652
  return __awaiter(this, void 0, void 0, function* () {
406
- const current = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
653
+ if (this.stopped)
654
+ return;
655
+ const current = this.identityProbe
656
+ ? this.identityProbeDevices.get(path)
657
+ : (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
407
658
  if (!(current === null || current === void 0 ? void 0 : current.opened))
408
659
  return;
660
+ this.assertHandleAvailable(current);
409
661
  const endpoints = this.deviceEndpoints.get(path);
410
662
  const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
411
663
  try {
412
- yield current.releaseInterface(ifaceNum);
664
+ yield this.runHandleOperation(() => current.releaseInterface(ifaceNum));
413
665
  }
414
666
  catch (error) {
415
667
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
416
668
  }
669
+ this.assertHandleAvailable(current);
417
670
  try {
418
- yield current.close();
671
+ yield this.runHandleOperation(() => current.close());
419
672
  }
420
673
  catch (error) {
421
674
  (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
675
+ if (this.identityProbe)
676
+ throw error;
422
677
  }
423
678
  });
424
679
  }
425
680
  clearEndpointHalt(device, direction, endpointNumber) {
426
681
  var _a;
427
682
  return __awaiter(this, void 0, void 0, function* () {
683
+ this.assertHandleAvailable(device);
428
684
  try {
429
- yield device.clearHalt(direction, endpointNumber);
685
+ yield this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
430
686
  }
431
687
  catch (error) {
688
+ this.assertRunning();
432
689
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`, error);
433
690
  }
434
691
  });
435
692
  }
436
693
  assertAcquired(path) {
694
+ this.assertRunning();
437
695
  if (!this.acquiredPaths.has(path)) {
438
696
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
439
697
  }
@@ -459,8 +717,79 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
459
717
  }
460
718
  return String(error);
461
719
  }
720
+ getErrorName(error) {
721
+ if (typeof error === 'object' && error && 'name' in error) {
722
+ const { name } = error;
723
+ return typeof name === 'string' ? name : String(name !== null && name !== void 0 ? name : '');
724
+ }
725
+ return '';
726
+ }
727
+ isWebUsbIoHardwareError(error) {
728
+ return (error instanceof hdShared.HardwareError &&
729
+ (error.errorCode === hdShared.HardwareErrorCode.BridgeDeviceDisconnected ||
730
+ error.errorCode === hdShared.HardwareErrorCode.BridgeNeedsPermission ||
731
+ error.errorCode === hdShared.HardwareErrorCode.WebUsbDeviceAccessError));
732
+ }
733
+ isDeviceStillEnumerated(path) {
734
+ var _a;
735
+ return __awaiter(this, void 0, void 0, function* () {
736
+ if (!this.usb)
737
+ return undefined;
738
+ try {
739
+ const devices = yield this.usb.getDevices();
740
+ return devices.some(device => hdShared.resolveOneKeyUsbDevicePath(device) === path);
741
+ }
742
+ catch (error) {
743
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] failed to verify WebUSB device presence:', error);
744
+ return undefined;
745
+ }
746
+ });
747
+ }
748
+ throwWebUsbIoError(path, operation, error) {
749
+ var _a;
750
+ return __awaiter(this, void 0, void 0, function* () {
751
+ this.assertRunning();
752
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
753
+ throw error;
754
+ if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
755
+ throw error;
756
+ const nativeError = error instanceof transport.ProtocolV2LinkError ? (_a = error.cause) !== null && _a !== void 0 ? _a : error : error;
757
+ if (this.isWebUsbIoHardwareError(nativeError))
758
+ throw nativeError;
759
+ const nativeErrorName = this.getErrorName(nativeError);
760
+ const nativeErrorMessage = this.getErrorMessage(nativeError);
761
+ if (this.isWebUsbPermissionDeniedMessage(nativeErrorMessage)) {
762
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BridgeNeedsPermission, nativeErrorMessage, {
763
+ operation,
764
+ nativeErrorName: nativeErrorName || undefined,
765
+ nativeErrorMessage,
766
+ });
767
+ }
768
+ const disconnectedByMessage = /(?:device )?disconnected|device not found|action was interrupted/i.test(`${nativeErrorName}: ${nativeErrorMessage}`);
769
+ const stillEnumerated = disconnectedByMessage
770
+ ? false
771
+ : yield this.isDeviceStillEnumerated(path);
772
+ const errorCode = disconnectedByMessage || stillEnumerated === false
773
+ ? hdShared.HardwareErrorCode.BridgeDeviceDisconnected
774
+ : hdShared.HardwareErrorCode.WebUsbDeviceAccessError;
775
+ throw hdShared.ERRORS.TypedError(errorCode, nativeErrorMessage || undefined, {
776
+ operation,
777
+ nativeErrorName: nativeErrorName || undefined,
778
+ nativeErrorMessage: nativeErrorMessage || undefined,
779
+ });
780
+ });
781
+ }
782
+ isWebUsbPermissionDeniedMessage(message) {
783
+ return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
784
+ }
462
785
  isRetryablePacketIoError(error) {
786
+ if (this.stopped)
787
+ return false;
788
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
789
+ return false;
463
790
  const message = this.getErrorMessage(error).toLowerCase();
791
+ if (this.isWebUsbPermissionDeniedMessage(message))
792
+ return false;
464
793
  return (message.includes('transferout') ||
465
794
  message.includes('transferin') ||
466
795
  message.includes('usbdevice') ||
@@ -475,21 +804,27 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
475
804
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
476
805
  this.staleProtocolPaths.add(path);
477
806
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
807
+ this.assertRunning();
478
808
  try {
479
809
  const currentDevice = yield this.findDevice(path);
810
+ this.assertHandleAvailable(currentDevice);
480
811
  if (currentDevice.opened) {
481
812
  const endpoints = this.deviceEndpoints.get(path);
482
813
  const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
483
814
  try {
484
- yield currentDevice.releaseInterface(ifaceNum);
815
+ yield this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
485
816
  }
486
817
  catch (releaseError) {
487
818
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
488
819
  }
489
- yield currentDevice.close();
820
+ this.assertHandleAvailable(currentDevice);
821
+ yield this.runHandleOperation(() => currentDevice.close());
490
822
  }
491
823
  }
492
824
  catch (closeError) {
825
+ this.assertRunning();
826
+ if (this.isHandleOwnershipError(closeError))
827
+ throw closeError;
493
828
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
494
829
  }
495
830
  yield this.getConnectedDevices();
@@ -525,18 +860,21 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
525
860
  lastError = error;
526
861
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
527
862
  if (!shouldRetry) {
528
- throw error;
863
+ return this.throwWebUsbIoError(path, 'transferOut', error);
529
864
  }
530
865
  try {
531
866
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
532
867
  }
533
868
  catch (reconnectError) {
869
+ this.assertRunning();
870
+ if (this.isHandleOwnershipError(reconnectError))
871
+ throw reconnectError;
534
872
  lastError = reconnectError;
535
873
  this.Log.debug(`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
536
874
  }
537
875
  }
538
876
  }
539
- throw lastError;
877
+ return this.throwWebUsbIoError(path, 'transferOut', lastError);
540
878
  });
541
879
  }
542
880
  transferOutOnce(path, packet) {
@@ -549,7 +887,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
549
887
  const endpoints = this.deviceEndpoints.get(path);
550
888
  const endpointOut = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointOut) !== null && _a !== void 0 ? _a : this.endpointId;
551
889
  const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
552
- yield device.transferOut(endpointOut, transferBuffer);
890
+ this.assertHandleAvailable(device);
891
+ const result = yield device.transferOut(endpointOut, transferBuffer);
892
+ this.assertHandleAvailable(device);
893
+ if (result.status !== 'ok') {
894
+ return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
895
+ }
553
896
  });
554
897
  }
555
898
  transferInWithRetry(path, length, cancelToken) {
@@ -567,7 +910,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
567
910
  }
568
911
  const endpoints = this.deviceEndpoints.get(path);
569
912
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
913
+ this.assertHandleAvailable(device);
570
914
  const result = yield device.transferIn(endpointIn, length);
915
+ this.assertHandleAvailable(device);
571
916
  return this.getTransferInData(result);
572
917
  }
573
918
  catch (error) {
@@ -577,18 +922,21 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
577
922
  }
578
923
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
579
924
  if (!shouldRetry) {
580
- throw error;
925
+ return this.throwWebUsbIoError(path, 'transferIn', error);
581
926
  }
582
927
  try {
583
928
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
584
929
  }
585
930
  catch (reconnectError) {
931
+ this.assertRunning();
932
+ if (this.isHandleOwnershipError(reconnectError))
933
+ throw reconnectError;
586
934
  lastError = reconnectError;
587
935
  this.Log.debug(`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
588
936
  }
589
937
  }
590
938
  }
591
- throw lastError;
939
+ return this.throwWebUsbIoError(path, 'transferIn', lastError);
592
940
  });
593
941
  }
594
942
  transferInOnce(path, length) {
@@ -600,7 +948,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
600
948
  }
601
949
  const endpoints = this.deviceEndpoints.get(path);
602
950
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
951
+ this.assertHandleAvailable(device);
603
952
  const result = yield device.transferIn(endpointIn, length);
953
+ this.assertHandleAvailable(device);
604
954
  return this.getTransferInData(result);
605
955
  });
606
956
  }
@@ -660,12 +1010,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
660
1010
  return false;
661
1011
  }
662
1012
  try {
663
- yield this.callProtocolV1(path, 'Initialize', {}, {
1013
+ const response = yield this.callProtocolV1(path, this.identityProbe ? 'GetFeatures' : 'Initialize', {}, {
664
1014
  timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
665
1015
  });
666
- return true;
1016
+ return !this.identityProbe || response.type === 'Features';
667
1017
  }
668
- catch (_error) {
1018
+ catch (error) {
1019
+ if (this.isWebUsbIoHardwareError(error))
1020
+ throw error;
669
1021
  return false;
670
1022
  }
671
1023
  });
@@ -680,6 +1032,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
680
1032
  timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
681
1033
  logger: this.Log,
682
1034
  logPrefix: 'ProtocolV2 WebUSB',
1035
+ shouldRethrow: error => this.isWebUsbIoHardwareError(error),
683
1036
  });
684
1037
  });
685
1038
  }
@@ -726,7 +1079,15 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
726
1079
  }
727
1080
  callProtocolV2(path, name, data, options) {
728
1081
  return __awaiter(this, void 0, void 0, function* () {
729
- return this.callProtocolV2Usb(path, name, data, options);
1082
+ try {
1083
+ return yield this.callProtocolV2Usb(path, name, data, options);
1084
+ }
1085
+ catch (error) {
1086
+ if (!(error instanceof transport.ProtocolV2LinkError) || error.code !== 'io')
1087
+ throw error;
1088
+ const operation = error.message.includes(' USB write failed:') ? 'transferOut' : 'transferIn';
1089
+ return this.throwWebUsbIoError(path, operation, error);
1090
+ }
730
1091
  });
731
1092
  }
732
1093
  receiveData(path, timeoutMs) {
@@ -769,8 +1130,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
769
1130
  release(path) {
770
1131
  return __awaiter(this, void 0, void 0, function* () {
771
1132
  this.acquiredPaths.delete(path);
772
- yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
773
- yield this.closeOpenDevice(path);
1133
+ try {
1134
+ yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1135
+ yield this.closeOpenDevice(path);
1136
+ }
1137
+ finally {
1138
+ this.releaseIdentityProbeHandle(path);
1139
+ this.releaseNormalHandles(path);
1140
+ }
774
1141
  this.deviceEndpoints.delete(path);
775
1142
  });
776
1143
  }
@@ -901,11 +1268,14 @@ class ElectronBleTransport {
901
1268
  if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
902
1269
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
903
1270
  }
1271
+ if (error.code === hdShared.HardwareErrorCode.BleLocationError) {
1272
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError);
1273
+ }
904
1274
  }
905
1275
  const errorMessage = error.message || String(error);
906
1276
  const poweredOffMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BlePoweredOff];
907
1277
  const unsupportedMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleUnsupported];
908
- const permissionMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BlePermissionError];
1278
+ const permissionMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleLocationError];
909
1279
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
910
1280
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
911
1281
  }
@@ -913,7 +1283,7 @@ class ElectronBleTransport {
913
1283
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
914
1284
  }
915
1285
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
916
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
1286
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError);
917
1287
  }
918
1288
  }
919
1289
  return error;
@@ -1174,10 +1544,12 @@ class ElectronBleTransport {
1174
1544
  });
1175
1545
  }
1176
1546
  createProtocolMismatchError(expected) {
1177
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1547
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
1548
+ expectedProtocol: expected,
1549
+ });
1178
1550
  }
1179
1551
  createProtocolDetectionError() {
1180
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
1552
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed);
1181
1553
  }
1182
1554
  clearProbeProtocol(uuid, protocol) {
1183
1555
  if (this.deviceProtocol.get(uuid) === protocol) {
@@ -1289,9 +1661,7 @@ class ElectronBleTransport {
1289
1661
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1290
1662
  this.resetProtocolV2Frames(uuid);
1291
1663
  },
1292
- shouldRethrow: error => expectedProtocol === 'V2' ||
1293
- hdShared.isBleStaleBondHardwareError(error) ||
1294
- transport.isProtocolV2LinkDisabledError(error),
1664
+ shouldRethrow: error => expectedProtocol === 'V2' || transport.isProtocolV2LinkDisabledError(error),
1295
1665
  });
1296
1666
  if (!detected) {
1297
1667
  this.clearProbeProtocol(uuid, 'V2');