@onekeyfe/hd-transport-web-device 1.2.0-alpha.140 → 1.2.0-alpha.141

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.
@@ -0,0 +1,244 @@
1
+ import transport from '@onekeyfe/hd-transport';
2
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+
4
+ import WebUsbTransport from '../src/webusb';
5
+
6
+ const schema = {
7
+ nested: {
8
+ Ping: { fields: { message: { type: 'string', id: 1 } } },
9
+ Success: { fields: { message: { type: 'string', id: 1 } } },
10
+ MessageType: {
11
+ values: {
12
+ MessageType_Ping: 60206,
13
+ MessageType_Success: 60207,
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ function buildAcquirableTransport(path = 'pro-webusb') {
20
+ const webusb = new WebUsbTransport() as any;
21
+ webusb.Log = { debug: jest.fn() };
22
+ webusb.rotateProtocolV2UsbGeneration = jest.fn().mockResolvedValue(undefined);
23
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
24
+ webusb.connect = jest.fn().mockResolvedValue(undefined);
25
+ // Simulate a device that stayed connected since the probe: same USBDevice
26
+ // object present in the device list and recorded as the probed object.
27
+ const deviceObject = { serialNumber: path } as unknown as USBDevice;
28
+ webusb.deviceList = [{ path, device: deviceObject, commType: 'webusb' }];
29
+ webusb.probedDeviceObjects.set(path, deviceObject);
30
+ return webusb;
31
+ }
32
+
33
+ describe('WebUsbTransport protocol probe cache', () => {
34
+ test('acquire skips the wire probe when the protocol is already cached', async () => {
35
+ const webusb = buildAcquirableTransport();
36
+ const path = 'pro-webusb';
37
+ webusb.deviceProtocol.set(path, 'V1');
38
+ webusb.detectProtocol = jest.fn();
39
+
40
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
41
+
42
+ expect(webusb.detectProtocol).not.toHaveBeenCalled();
43
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
44
+ expect(webusb.acquiredPaths.has(path)).toBe(true);
45
+ });
46
+
47
+ test('acquire re-probes when the caller expects a different protocol than cached', async () => {
48
+ const webusb = buildAcquirableTransport();
49
+ const path = 'pro-webusb';
50
+ webusb.deviceProtocol.set(path, 'V1');
51
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
52
+ webusb.deviceProtocol.set(p, 'V2');
53
+ return Promise.resolve('V2');
54
+ });
55
+
56
+ await expect(webusb.acquire({ path, expectedProtocol: 'V2' })).resolves.toBe(path);
57
+
58
+ expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V2', undefined);
59
+ expect(webusb.deviceProtocol.get(path)).toBe('V2');
60
+ });
61
+
62
+ test('acquire probes when nothing is cached for the path', async () => {
63
+ const webusb = buildAcquirableTransport();
64
+ const path = 'pro-webusb';
65
+ webusb.detectProtocol = jest.fn().mockResolvedValue('V1');
66
+
67
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
68
+
69
+ expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V1', undefined);
70
+ });
71
+
72
+ test('forced protocol detection bypasses a valid cache and probes on the wire', async () => {
73
+ const webusb = buildAcquirableTransport();
74
+ const path = 'pro-webusb';
75
+ // Valid cache AND unchanged USBDevice object — the strongest cache hit.
76
+ webusb.deviceProtocol.set(path, 'V1');
77
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
78
+ webusb.deviceProtocol.set(p, 'V1');
79
+ return Promise.resolve('V1');
80
+ });
81
+
82
+ await expect(webusb.acquire({ path, forceProtocolDetection: true })).resolves.toBe(path);
83
+
84
+ // Explicit recovery must always reach the wire probe.
85
+ expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ test('acquire re-probes when the USBDevice object identity changed since the probe', async () => {
89
+ const webusb = buildAcquirableTransport();
90
+ const path = 'pro-webusb';
91
+ webusb.deviceProtocol.set(path, 'V1');
92
+ // Simulate a replug the transport never saw a disconnect event for: the OS
93
+ // re-enumerated the device, so the list now holds a NEW USBDevice object.
94
+ webusb.deviceList = [
95
+ { path, device: { serialNumber: path } as unknown as USBDevice, commType: 'webusb' },
96
+ ];
97
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
98
+ webusb.deviceProtocol.set(p, 'V1');
99
+ return Promise.resolve('V1');
100
+ });
101
+
102
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
103
+
104
+ expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
105
+ });
106
+
107
+ test('acquire re-probes a stale-marked path even when a protocol is cached', async () => {
108
+ const webusb = buildAcquirableTransport();
109
+ const path = 'pro-webusb';
110
+ webusb.deviceProtocol.set(path, 'V1');
111
+ webusb.markProtocolStale(path);
112
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
113
+ webusb.deviceProtocol.set(p, 'V1');
114
+ return Promise.resolve('V1');
115
+ });
116
+
117
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
118
+
119
+ expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
120
+ expect(webusb.staleProtocolPaths.has(path)).toBe(false);
121
+ });
122
+
123
+ test('release keeps the cached V1 protocol so the next acquire can reuse it', async () => {
124
+ const webusb = new WebUsbTransport() as any;
125
+ const path = 'pro-webusb';
126
+ webusb.deviceProtocol.set(path, 'V1');
127
+ webusb.deviceProtocolHints.set(path, 'V2');
128
+ webusb.deviceEndpoints.set(path, { interfaceNumber: 0, endpointIn: 1, endpointOut: 1 });
129
+ webusb.acquiredPaths.add(path);
130
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
131
+
132
+ await webusb.release(path);
133
+
134
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
135
+ expect(webusb.deviceProtocolHints.get(path)).toBe('V2');
136
+ expect(webusb.deviceEndpoints.has(path)).toBe(false);
137
+ expect(webusb.acquiredPaths.has(path)).toBe(false);
138
+ });
139
+
140
+ test('release still drops a cached V2 protocol through link invalidation', async () => {
141
+ const webusb = new WebUsbTransport() as any;
142
+ const path = 'pro2-webusb';
143
+ webusb.Log = { debug: jest.fn() };
144
+ webusb.messages = transport.parseConfigure(schema);
145
+ webusb.messagesV2 = transport.parseConfigure(schema);
146
+ webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
147
+ let markReadStarted: () => void = () => undefined;
148
+ const readStarted = new Promise<void>(resolve => {
149
+ markReadStarted = resolve;
150
+ });
151
+ webusb.readProtocolV2UsbPacket = jest.fn().mockImplementation(() => {
152
+ markReadStarted();
153
+ return new Promise<void>(() => {});
154
+ });
155
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
156
+ webusb.deviceProtocol.set(path, 'V2');
157
+ await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
158
+
159
+ const call = webusb.callProtocolV2(path, 'Ping', { message: 'release' });
160
+ await readStarted;
161
+ await webusb.release(path);
162
+
163
+ await expect(call).rejects.toThrow('WebUSB transport released');
164
+ expect(webusb.deviceProtocol.has(path)).toBe(false);
165
+ });
166
+
167
+ test('call and post fail fast for a path that is not acquired', async () => {
168
+ const webusb = new WebUsbTransport() as any;
169
+ const path = 'pro-webusb';
170
+ webusb.Log = { debug: jest.fn() };
171
+ webusb.messages = transport.parseConfigure(schema);
172
+ webusb.messagesV2 = transport.parseConfigure(schema);
173
+ // A surviving protocol cache entry must NOT act as a session token.
174
+ webusb.deviceProtocol.set(path, 'V1');
175
+
176
+ await expect(webusb.call(path, 'Ping', {})).rejects.toMatchObject({
177
+ errorCode: HardwareErrorCode.RuntimeError,
178
+ message: expect.stringContaining('not acquired'),
179
+ });
180
+ await expect(webusb.post(path, 'Ping', {})).rejects.toMatchObject({
181
+ errorCode: HardwareErrorCode.RuntimeError,
182
+ message: expect.stringContaining('not acquired'),
183
+ });
184
+ });
185
+
186
+ test('a transfer-level reconnect marks the protocol stale for the next acquire', async () => {
187
+ const webusb = new WebUsbTransport() as any;
188
+ const path = 'pro-webusb';
189
+ webusb.Log = { debug: jest.fn() };
190
+ webusb.deviceProtocol.set(path, 'V1');
191
+ webusb.findDevice = jest.fn().mockResolvedValue({ opened: false });
192
+ webusb.getConnectedDevices = jest.fn().mockResolvedValue([]);
193
+ webusb.connect = jest.fn().mockResolvedValue(undefined);
194
+
195
+ await webusb.reconnectForPacketIoRetry(path, 'in', 0, new Error('transferIn failed'));
196
+
197
+ // The in-flight call keeps the cached protocol; only the next acquire re-probes.
198
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
199
+ expect(webusb.staleProtocolPaths.has(path)).toBe(true);
200
+ });
201
+
202
+ test('USB disconnect marks the serial stale and the listener attaches only once', () => {
203
+ const addEventListener = jest.fn();
204
+ const usb = { addEventListener } as unknown as USB;
205
+ const originalNavigator = (globalThis as any).navigator;
206
+ Object.defineProperty(globalThis, 'navigator', {
207
+ value: { usb },
208
+ configurable: true,
209
+ });
210
+ try {
211
+ const first = new WebUsbTransport() as any;
212
+ first.init({ debug: jest.fn() });
213
+ const second = new WebUsbTransport() as any;
214
+ second.init({ debug: jest.fn() });
215
+
216
+ // Module-level listener: one registration across instances.
217
+ expect(addEventListener).toHaveBeenCalledTimes(1);
218
+ const handler = addEventListener.mock.calls[0][1] as (event: {
219
+ device?: { serialNumber?: string | null };
220
+ }) => void;
221
+
222
+ const path = 'pro-webusb';
223
+ first.deviceProtocol.set(path, 'V1');
224
+ second.deviceProtocol.set(path, 'V1');
225
+ handler({ device: { serialNumber: path } });
226
+
227
+ // Routed to the most recently initialized instance; the cached value is
228
+ // retained (in-flight sessions keep working) but marked stale.
229
+ expect(second.staleProtocolPaths.has(path)).toBe(true);
230
+ expect(second.deviceProtocol.get(path)).toBe('V1');
231
+ expect(first.staleProtocolPaths.has(path)).toBe(false);
232
+
233
+ // Events without a usable serial are ignored.
234
+ handler({ device: { serialNumber: null } });
235
+ handler({});
236
+ expect(second.staleProtocolPaths.size).toBe(1);
237
+ } finally {
238
+ Object.defineProperty(globalThis, 'navigator', {
239
+ value: originalNavigator,
240
+ configurable: true,
241
+ });
242
+ }
243
+ });
244
+ });
package/dist/index.d.ts CHANGED
@@ -20,6 +20,21 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
20
20
  /** Per-path protocol type detected by active wire-level probe. */
21
21
  private deviceProtocol;
22
22
  private deviceProtocolHints;
23
+ /**
24
+ * Paths whose cached protocol must be re-probed on the next acquire (a USB
25
+ * disconnect was seen, or a transfer-level reconnect happened mid-call).
26
+ */
27
+ private staleProtocolPaths;
28
+ /** Paths currently acquired (between a successful acquire() and its release()). */
29
+ private acquiredPaths;
30
+ /**
31
+ * The exact USBDevice object each cached protocol was probed against. The
32
+ * browser returns the same object identity for a device as long as it stays
33
+ * connected, and a replug/reboot always yields a new object — so an identity
34
+ * mismatch proves the device was re-enumerated since the probe, even when the
35
+ * disconnect event itself was delayed or missed.
36
+ */
37
+ private probedDeviceObjects;
23
38
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
24
39
  private deviceEndpoints;
25
40
  name: string;
@@ -40,6 +55,17 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
40
55
  * Initialize WebUSB transport
41
56
  */
42
57
  init(logger: any): void;
58
+ /**
59
+ * Protocol type is a property of the physical device keyed by USB serial number.
60
+ * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
61
+ * a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
62
+ * reconnects, which cover a missed disconnect event) only MARK the cached probe
63
+ * result stale instead of deleting it: an in-flight session keeps using the old
64
+ * value so the transfer-level reconnect retries can absorb a transient
65
+ * re-enumeration exactly as they did before the cache existed, while the next
66
+ * acquire re-probes from scratch.
67
+ */
68
+ markProtocolStale(path: string): void;
43
69
  /**
44
70
  * Configure Protocol V1 protobuf schema (legacy chunked 0x3F framing).
45
71
  */
@@ -97,6 +123,14 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
97
123
  connectToDevice(path: string, first: boolean): Promise<void>;
98
124
  private closeOpenDevice;
99
125
  private clearEndpointHalt;
126
+ /**
127
+ * With the protocol cache surviving release(), deviceProtocol presence no
128
+ * longer implies an active session. Guard call()/post() explicitly so a
129
+ * post-release straggler fails fast instead of silently reopening the device
130
+ * and driving it outside any session (pre-cache behavior: the deleted
131
+ * protocol entry produced the same fail-fast).
132
+ */
133
+ private assertAcquired;
100
134
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
101
135
  private getErrorMessage;
102
136
  private isRetryablePacketIoError;
package/dist/index.js CHANGED
@@ -57,6 +57,21 @@ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
57
57
  function inferProtocolHintFromDeviceName$1(name) {
58
58
  return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
59
59
  }
60
+ let activeWebUsbTransport;
61
+ let usbDisconnectListenerAttached = false;
62
+ function registerActiveWebUsbTransport(instance, usb) {
63
+ activeWebUsbTransport = instance;
64
+ if (usbDisconnectListenerAttached)
65
+ return;
66
+ usbDisconnectListenerAttached = true;
67
+ usb.addEventListener('disconnect', event => {
68
+ var _a;
69
+ const serial = (_a = event.device) === null || _a === void 0 ? void 0 : _a.serialNumber;
70
+ if (typeof serial !== 'string' || serial.length === 0)
71
+ return;
72
+ activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.markProtocolStale(serial);
73
+ });
74
+ }
60
75
  class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
61
76
  constructor() {
62
77
  super({
@@ -66,6 +81,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
66
81
  });
67
82
  this.deviceProtocol = new Map();
68
83
  this.deviceProtocolHints = new Map();
84
+ this.staleProtocolPaths = new Set();
85
+ this.acquiredPaths = new Set();
86
+ this.probedDeviceObjects = new Map();
69
87
  this.deviceEndpoints = new Map();
70
88
  this.name = 'WebUsbTransport';
71
89
  this.stopped = false;
@@ -82,6 +100,11 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
82
100
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
83
101
  }
84
102
  this.usb = usb;
103
+ registerActiveWebUsbTransport(this, usb);
104
+ }
105
+ markProtocolStale(path) {
106
+ this.staleProtocolPaths.add(path);
107
+ this.probedDeviceObjects.delete(path);
85
108
  }
86
109
  configure(signedData) {
87
110
  const messages = parseConfigure$1(signedData);
@@ -148,7 +171,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
148
171
  });
149
172
  }
150
173
  acquire(input) {
151
- var _a, _b, _c, _d;
174
+ var _a, _b, _c, _d, _e, _f;
152
175
  return __awaiter(this, void 0, void 0, function* () {
153
176
  if (!input.path)
154
177
  return;
@@ -156,18 +179,45 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
156
179
  yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
157
180
  yield this.closeOpenDevice(input.path);
158
181
  yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
159
- const deviceName = (_b = this.deviceList.find(device => device.path === input.path)) === null || _b === void 0 ? void 0 : _b.device.productName;
160
- const protocolHint = input.expectedProtocol
161
- ? undefined
162
- : (_d = (_c = input.protocolHint) !== null && _c !== void 0 ? _c : this.deviceProtocolHints.get(input.path)) !== null && _d !== void 0 ? _d : inferProtocolHintFromDeviceName$1(deviceName);
163
- if (protocolHint) {
164
- this.deviceProtocolHints.set(input.path, protocolHint);
182
+ if (input.forceProtocolDetection) {
183
+ this.staleProtocolPaths.delete(input.path);
184
+ this.deviceProtocol.delete(input.path);
185
+ this.deviceProtocolHints.delete(input.path);
186
+ }
187
+ if (this.staleProtocolPaths.has(input.path)) {
188
+ this.staleProtocolPaths.delete(input.path);
189
+ this.deviceProtocol.delete(input.path);
190
+ }
191
+ if (this.deviceProtocol.has(input.path)) {
192
+ const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
193
+ if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
194
+ this.deviceProtocol.delete(input.path);
195
+ }
196
+ }
197
+ const cachedProtocol = this.deviceProtocol.get(input.path);
198
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
199
+ this.deviceProtocol.delete(input.path);
165
200
  }
166
- yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
201
+ if (!this.deviceProtocol.has(input.path)) {
202
+ const deviceName = (_c = this.deviceList.find(device => device.path === input.path)) === null || _c === void 0 ? void 0 : _c.device.productName;
203
+ const protocolHint = input.expectedProtocol
204
+ ? undefined
205
+ : (_e = (_d = input.protocolHint) !== null && _d !== void 0 ? _d : this.deviceProtocolHints.get(input.path)) !== null && _e !== void 0 ? _e : inferProtocolHintFromDeviceName$1(deviceName);
206
+ if (protocolHint) {
207
+ this.deviceProtocolHints.set(input.path, protocolHint);
208
+ }
209
+ yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
210
+ const probedDevice = (_f = this.deviceList.find(d => d.path === input.path)) === null || _f === void 0 ? void 0 : _f.device;
211
+ if (probedDevice) {
212
+ this.probedDeviceObjects.set(input.path, probedDevice);
213
+ }
214
+ }
215
+ this.acquiredPaths.add(input.path);
167
216
  return yield Promise.resolve(input.path);
168
217
  }
169
218
  catch (e) {
170
219
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
220
+ this.acquiredPaths.delete(input.path);
171
221
  yield this.closeOpenDevice(input.path);
172
222
  throw e;
173
223
  }
@@ -351,8 +401,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
351
401
  }
352
402
  });
353
403
  }
404
+ assertAcquired(path) {
405
+ if (!this.acquiredPaths.has(path)) {
406
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
407
+ }
408
+ }
354
409
  post(session, name, data) {
355
410
  return __awaiter(this, void 0, void 0, function* () {
411
+ this.assertAcquired(session);
356
412
  if (this.deviceProtocol.get(session) === 'V2') {
357
413
  yield this.sendProtocolV2UsbFlowControl(session, name, data);
358
414
  return;
@@ -385,6 +441,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
385
441
  var _a;
386
442
  return __awaiter(this, void 0, void 0, function* () {
387
443
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
444
+ this.staleProtocolPaths.add(path);
388
445
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
389
446
  try {
390
447
  const currentDevice = yield this.findDevice(path);
@@ -599,6 +656,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
599
656
  if (this.messages == null) {
600
657
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
601
658
  }
659
+ this.assertAcquired(path);
602
660
  const device = yield this.findDevice(path);
603
661
  if (!device) {
604
662
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
@@ -678,10 +736,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
678
736
  }
679
737
  release(path) {
680
738
  return __awaiter(this, void 0, void 0, function* () {
739
+ this.acquiredPaths.delete(path);
681
740
  yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
682
741
  yield this.closeOpenDevice(path);
683
- this.deviceProtocol.delete(path);
684
- this.deviceProtocolHints.delete(path);
685
742
  this.deviceEndpoints.delete(path);
686
743
  });
687
744
  }
package/dist/webusb.d.ts CHANGED
@@ -12,6 +12,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
12
12
  private protocolV2SchemaSource;
13
13
  private deviceProtocol;
14
14
  private deviceProtocolHints;
15
+ private staleProtocolPaths;
16
+ private acquiredPaths;
17
+ private probedDeviceObjects;
15
18
  private deviceEndpoints;
16
19
  name: string;
17
20
  stopped: boolean;
@@ -24,6 +27,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
24
27
  interfaceId: number;
25
28
  constructor();
26
29
  init(logger: any): void;
30
+ markProtocolStale(path: string): void;
27
31
  configure(signedData: any): void;
28
32
  configureProtocolV2(signedData: any): void;
29
33
  promptDeviceAccess(): Promise<USBDevice | null>;
@@ -40,6 +44,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
40
44
  connectToDevice(path: string, first: boolean): Promise<void>;
41
45
  private closeOpenDevice;
42
46
  private clearEndpointHalt;
47
+ private assertAcquired;
43
48
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
44
49
  private getErrorMessage;
45
50
  private isRetryablePacketIoError;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.0-alpha.140",
3
+ "version": "1.2.0-alpha.141",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.140",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.140"
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.141",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.141"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.0-alpha.140",
28
+ "@onekeyfe/hd-transport-electron": "1.2.0-alpha.141",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "15e3f0dc6c543c83a96e63b5d19aad70cdd6efb1"
32
+ "gitHead": "2fdadf97749fea76cf47f5b1899f18050abe01d1"
33
33
  }
package/src/webusb.ts CHANGED
@@ -69,6 +69,27 @@ interface TransferCancelToken {
69
69
  cancelled: boolean;
70
70
  }
71
71
 
72
+ /**
73
+ * The navigator.usb disconnect listener is module-scoped and attached at most
74
+ * once: listeners on navigator.usb are global and never garbage collected, so a
75
+ * per-instance listener would retain every transport instance created across
76
+ * SDK re-initializations. Events are routed to the most recently initialized
77
+ * transport instance instead.
78
+ */
79
+ let activeWebUsbTransport: WebUsbTransport | undefined;
80
+ let usbDisconnectListenerAttached = false;
81
+
82
+ function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
83
+ activeWebUsbTransport = instance;
84
+ if (usbDisconnectListenerAttached) return;
85
+ usbDisconnectListenerAttached = true;
86
+ usb.addEventListener('disconnect', event => {
87
+ const serial = event.device?.serialNumber;
88
+ if (typeof serial !== 'string' || serial.length === 0) return;
89
+ activeWebUsbTransport?.markProtocolStale(serial);
90
+ });
91
+ }
92
+
72
93
  export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
73
94
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
74
95
 
@@ -82,6 +103,24 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
82
103
 
83
104
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
84
105
 
106
+ /**
107
+ * Paths whose cached protocol must be re-probed on the next acquire (a USB
108
+ * disconnect was seen, or a transfer-level reconnect happened mid-call).
109
+ */
110
+ private staleProtocolPaths: Set<string> = new Set();
111
+
112
+ /** Paths currently acquired (between a successful acquire() and its release()). */
113
+ private acquiredPaths: Set<string> = new Set();
114
+
115
+ /**
116
+ * The exact USBDevice object each cached protocol was probed against. The
117
+ * browser returns the same object identity for a device as long as it stays
118
+ * connected, and a replug/reboot always yields a new object — so an identity
119
+ * mismatch proves the device was re-enumerated since the probe, even when the
120
+ * disconnect event itself was delayed or missed.
121
+ */
122
+ private probedDeviceObjects: Map<string, USBDevice> = new Map();
123
+
85
124
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
86
125
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
87
126
 
@@ -129,6 +168,22 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
129
168
  );
130
169
  }
131
170
  this.usb = usb;
171
+ registerActiveWebUsbTransport(this, usb);
172
+ }
173
+
174
+ /**
175
+ * Protocol type is a property of the physical device keyed by USB serial number.
176
+ * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
177
+ * a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
178
+ * reconnects, which cover a missed disconnect event) only MARK the cached probe
179
+ * result stale instead of deleting it: an in-flight session keeps using the old
180
+ * value so the transfer-level reconnect retries can absorb a transient
181
+ * re-enumeration exactly as they did before the cache existed, while the next
182
+ * acquire re-probes from scratch.
183
+ */
184
+ markProtocolStale(path: string) {
185
+ this.staleProtocolPaths.add(path);
186
+ this.probedDeviceObjects.delete(path);
132
187
  }
133
188
 
134
189
  /**
@@ -231,20 +286,57 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
231
286
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
232
287
  await this.closeOpenDevice(input.path);
233
288
  await this.connect(input.path ?? '', true);
234
- const deviceName = this.deviceList.find(device => device.path === input.path)?.device
235
- .productName;
236
- const protocolHint = input.expectedProtocol
237
- ? undefined
238
- : input.protocolHint ??
239
- this.deviceProtocolHints.get(input.path) ??
240
- inferProtocolHintFromDeviceName(deviceName);
241
- if (protocolHint) {
242
- this.deviceProtocolHints.set(input.path, protocolHint);
289
+ if (input.forceProtocolDetection) {
290
+ // Explicit recovery/discovery (e.g. detectDeviceConnectProtocol) must
291
+ // probe on the wire regardless of any cached result — the cached
292
+ // binding may be exactly what the caller is trying to recover from.
293
+ this.staleProtocolPaths.delete(input.path);
294
+ this.deviceProtocol.delete(input.path);
295
+ this.deviceProtocolHints.delete(input.path);
296
+ }
297
+ if (this.staleProtocolPaths.has(input.path)) {
298
+ // The device disconnected (possibly rebooting into another mode) since
299
+ // the protocol was probed — drop the cache so it is re-probed below.
300
+ this.staleProtocolPaths.delete(input.path);
301
+ this.deviceProtocol.delete(input.path);
302
+ }
303
+ if (this.deviceProtocol.has(input.path)) {
304
+ const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
305
+ if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
306
+ // The OS re-enumerated the device since the probe (a replug/reboot we
307
+ // may not have seen a disconnect event for) — the cached protocol can
308
+ // no longer be trusted; re-probe on the wire below.
309
+ this.deviceProtocol.delete(input.path);
310
+ }
311
+ }
312
+ const cachedProtocol = this.deviceProtocol.get(input.path);
313
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
314
+ // The caller expects a different protocol than the cached probe result;
315
+ // the cache is stale — drop it and re-probe on the wire below.
316
+ this.deviceProtocol.delete(input.path);
317
+ }
318
+ if (!this.deviceProtocol.has(input.path)) {
319
+ const deviceName = this.deviceList.find(device => device.path === input.path)?.device
320
+ .productName;
321
+ const protocolHint = input.expectedProtocol
322
+ ? undefined
323
+ : input.protocolHint ??
324
+ this.deviceProtocolHints.get(input.path) ??
325
+ inferProtocolHintFromDeviceName(deviceName);
326
+ if (protocolHint) {
327
+ this.deviceProtocolHints.set(input.path, protocolHint);
328
+ }
329
+ await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
330
+ const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
331
+ if (probedDevice) {
332
+ this.probedDeviceObjects.set(input.path, probedDevice);
333
+ }
243
334
  }
244
- await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
335
+ this.acquiredPaths.add(input.path);
245
336
  return await Promise.resolve(input.path);
246
337
  } catch (e) {
247
338
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
339
+ this.acquiredPaths.delete(input.path);
248
340
  await this.closeOpenDevice(input.path);
249
341
  throw e;
250
342
  }
@@ -480,7 +572,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
480
572
  }
481
573
  }
482
574
 
575
+ /**
576
+ * With the protocol cache surviving release(), deviceProtocol presence no
577
+ * longer implies an active session. Guard call()/post() explicitly so a
578
+ * post-release straggler fails fast instead of silently reopening the device
579
+ * and driving it outside any session (pre-cache behavior: the deleted
580
+ * protocol entry produced the same fail-fast).
581
+ */
582
+ private assertAcquired(path: string) {
583
+ if (!this.acquiredPaths.has(path)) {
584
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
585
+ }
586
+ }
587
+
483
588
  async post(session: string, name: string, data: Record<string, unknown>) {
589
+ this.assertAcquired(session);
484
590
  if (this.deviceProtocol.get(session) === 'V2') {
485
591
  await this.sendProtocolV2UsbFlowControl(session, name, data);
486
592
  return;
@@ -522,6 +628,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
522
628
  error
523
629
  )}`
524
630
  );
631
+ // The device dropped off the bus mid-call and may have rebooted into a
632
+ // different mode. The in-flight retry keeps the known protocol, but the
633
+ // next acquire must re-probe — this also self-heals a stale cache when the
634
+ // USB disconnect event itself was missed.
635
+ this.staleProtocolPaths.add(path);
525
636
  await wait(attempt * PACKET_IO_RETRY_DELAY);
526
637
 
527
638
  try {
@@ -755,6 +866,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
755
866
  if (this.messages == null) {
756
867
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
757
868
  }
869
+ this.assertAcquired(path);
758
870
 
759
871
  const device = await this.findDevice(path);
760
872
  if (!device) {
@@ -871,10 +983,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
871
983
  * Release device
872
984
  */
873
985
  async release(path: string) {
986
+ this.acquiredPaths.delete(path);
874
987
  await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
875
988
  await this.closeOpenDevice(path);
876
- this.deviceProtocol.delete(path);
877
- this.deviceProtocolHints.delete(path);
989
+ // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
990
+ // physical-device property, so the next acquire can skip the wire-level probe.
991
+ // V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
992
+ // invalidation above, and any entry is re-probed after a USB disconnect or a
993
+ // transfer-level reconnect (see markProtocolStale).
878
994
  this.deviceEndpoints.delete(path);
879
995
  }
880
996