@onekeyfe/hd-core 1.2.0-alpha.98 → 1.2.0

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.
Files changed (204) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +203 -27
  3. package/__tests__/base64Data.test.ts +69 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +89 -38
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +83 -14
  6. package/__tests__/device-connector-protocol.test.ts +21 -0
  7. package/__tests__/device-lifecycle-events.test.ts +427 -7
  8. package/__tests__/device-pool-state.test.ts +25 -0
  9. package/__tests__/device-settings.test.ts +237 -9
  10. package/__tests__/device-state-events.test.ts +2 -2
  11. package/__tests__/device-state-mapper.test.ts +61 -5
  12. package/__tests__/device-utils.test.ts +15 -1
  13. package/__tests__/device-wallet-session-store.test.ts +147 -21
  14. package/__tests__/deviceSettings.test.ts +7 -3
  15. package/__tests__/deviceUploadNft.test.ts +36 -4
  16. package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -3
  17. package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
  18. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
  19. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
  20. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +128 -2
  21. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  22. package/__tests__/firmware-update/firmware-update-plan.test.ts +114 -50
  23. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  24. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  25. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +678 -0
  26. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  27. package/__tests__/get-device-state.test.ts +60 -21
  28. package/__tests__/logBlockEvent.test.ts +13 -1
  29. package/__tests__/open-wallet-session.test.ts +127 -1
  30. package/__tests__/protocol-v2-firmware-targets.test.ts +39 -0
  31. package/__tests__/protocol-v2-resources.test.ts +185 -88
  32. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  33. package/__tests__/protocol-v2.test.ts +2274 -466
  34. package/__tests__/refresh-device-state.test.ts +3 -1
  35. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  36. package/__tests__/search-devices.test.ts +79 -32
  37. package/__tests__/ton-sign-message.test.ts +84 -0
  38. package/dist/api/BaseMethod.d.ts +2 -1
  39. package/dist/api/BaseMethod.d.ts.map +1 -1
  40. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV4.d.ts +22 -7
  45. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  46. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  47. package/dist/api/SearchDevices.d.ts.map +1 -1
  48. package/dist/api/UploadPortfolio.d.ts +1 -1
  49. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  50. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  51. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  54. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  55. package/dist/api/device/DeviceVerify.d.ts +1 -0
  56. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  57. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  59. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  61. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  63. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  65. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  66. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
  67. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  68. package/dist/api/firmware/getBinary.d.ts +3 -2
  69. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  70. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  71. package/dist/api/helpers/base64Data.d.ts +16 -0
  72. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  73. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  74. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  76. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  77. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  78. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  79. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  80. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  81. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  82. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  83. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  84. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  85. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  86. package/dist/core/index.d.ts +6 -1
  87. package/dist/core/index.d.ts.map +1 -1
  88. package/dist/data-manager/DataManager.d.ts +4 -3
  89. package/dist/data-manager/DataManager.d.ts.map +1 -1
  90. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  91. package/dist/device/Device.d.ts +16 -3
  92. package/dist/device/Device.d.ts.map +1 -1
  93. package/dist/device/DeviceCommands.d.ts +4 -4
  94. package/dist/device/DeviceCommands.d.ts.map +1 -1
  95. package/dist/device/DeviceConnector.d.ts +1 -1
  96. package/dist/device/DeviceConnector.d.ts.map +1 -1
  97. package/dist/device/DevicePool.d.ts.map +1 -1
  98. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  99. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  100. package/dist/events/logBlockEvent.d.ts.map +1 -1
  101. package/dist/events/ui-request.d.ts +3 -0
  102. package/dist/events/ui-request.d.ts.map +1 -1
  103. package/dist/index.d.ts +54 -84
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2333 -1515
  106. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  107. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  108. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  109. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  110. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  111. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  113. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  114. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  115. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  116. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  117. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  118. package/dist/types/api/protocolV2.d.ts +1 -1
  119. package/dist/types/api/protocolV2.d.ts.map +1 -1
  120. package/dist/types/device.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +3 -31
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  124. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  125. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  126. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  127. package/dist/utils/deviceSettings.d.ts +1 -0
  128. package/dist/utils/deviceSettings.d.ts.map +1 -1
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/patch.d.ts +1 -1
  132. package/dist/utils/patch.d.ts.map +1 -1
  133. package/dist/utils/pro2Nft.d.ts +7 -0
  134. package/dist/utils/pro2Nft.d.ts.map +1 -1
  135. package/package.json +6 -4
  136. package/src/api/BaseMethod.ts +8 -1
  137. package/src/api/CheckAllFirmwareRelease.ts +8 -5
  138. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  139. package/src/api/CheckBootloaderRelease.ts +3 -2
  140. package/src/api/CheckFirmwareRelease.ts +2 -2
  141. package/src/api/FirmwareUpdateV2.ts +130 -69
  142. package/src/api/FirmwareUpdateV3.ts +29 -19
  143. package/src/api/FirmwareUpdateV4.ts +966 -743
  144. package/src/api/OpenWalletSession.ts +39 -9
  145. package/src/api/PromptWebDeviceAccess.ts +2 -2
  146. package/src/api/SearchDevices.ts +6 -3
  147. package/src/api/UploadPortfolio.ts +9 -2
  148. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  149. package/src/api/device/DeviceChangePin.ts +4 -1
  150. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  151. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  152. package/src/api/device/DeviceSettings.ts +34 -20
  153. package/src/api/device/DeviceVerify.ts +25 -0
  154. package/src/api/device/DeviceWipe.ts +4 -1
  155. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  156. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  157. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  158. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +131 -76
  159. package/src/api/firmware/FirmwareUpdatePlan.ts +55 -82
  160. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +32 -15
  161. package/src/api/firmware/protocolV2Release.ts +7 -2
  162. package/src/api/helpers/base64Data.ts +85 -0
  163. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  164. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  165. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  166. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  167. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  168. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  169. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  170. package/src/api/protocol-v2/helpers.ts +8 -0
  171. package/src/api/ton/TonSignMessage.ts +5 -3
  172. package/src/core/index.ts +264 -55
  173. package/src/data/messages/messages-protocol-v2.json +140 -13
  174. package/src/data-manager/DataManager.ts +46 -39
  175. package/src/data-manager/TransportManager.ts +3 -1
  176. package/src/device/Device.ts +210 -44
  177. package/src/device/DeviceCommands.ts +27 -18
  178. package/src/device/DeviceConnector.ts +17 -3
  179. package/src/device/DevicePool.ts +9 -2
  180. package/src/device/DeviceStateMapper.ts +30 -24
  181. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  182. package/src/events/logBlockEvent.ts +14 -1
  183. package/src/events/ui-request.ts +3 -0
  184. package/src/index.ts +0 -10
  185. package/src/protocols/protocol-v2/features.ts +22 -5
  186. package/src/protocols/protocol-v2/index.ts +2 -0
  187. package/src/protocols/protocol-v2/resources.ts +110 -147
  188. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  189. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  190. package/src/types/api/firmwareUpdate.ts +1 -1
  191. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  192. package/src/types/api/protocolV2.ts +1 -1
  193. package/src/types/device.ts +2 -1
  194. package/src/types/settings.ts +6 -34
  195. package/src/utils/deviceFeaturesCompat.ts +9 -4
  196. package/src/utils/deviceFeaturesUtils.ts +4 -0
  197. package/src/utils/deviceInfoUtils.ts +9 -2
  198. package/src/utils/deviceSettings.ts +11 -4
  199. package/src/utils/index.ts +1 -0
  200. package/src/utils/pro2Nft.ts +31 -8
  201. package/__tests__/firmware-memory-host.test.ts +0 -112
  202. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  203. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  204. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
@@ -203,6 +203,69 @@ describe.each([
203
203
  });
204
204
  });
205
205
 
206
+ describe('FirmwareUpdateV2 WebUSB bootloader polling', () => {
207
+ beforeEach(() => {
208
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
209
+ });
210
+
211
+ afterEach(() => {
212
+ jest.clearAllTimers();
213
+ jest.useRealTimers();
214
+ jest.restoreAllMocks();
215
+ });
216
+
217
+ test('skips interval ticks while a bootloader probe is in flight and resumes afterward', async () => {
218
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop' as never);
219
+ jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
220
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
221
+
222
+ const method = new FirmwareUpdateV2({
223
+ id: 1,
224
+ payload: { method: 'firmwareUpdateV2', connectId: WEBUSB_ID },
225
+ });
226
+
227
+ let inFlight = 0;
228
+ let maxInFlight = 0;
229
+ const resolveProbes: Array<(found: boolean) => void> = [];
230
+ const probe = jest.spyOn(method as any, '_checkDeviceInBootloaderMode').mockImplementation(
231
+ () =>
232
+ new Promise<boolean>(resolve => {
233
+ inFlight += 1;
234
+ maxInFlight = Math.max(maxInFlight, inFlight);
235
+ resolveProbes.push(found => {
236
+ inFlight -= 1;
237
+ resolve(found);
238
+ });
239
+ })
240
+ );
241
+
242
+ method.device = {
243
+ getCurrentDeviceType: jest.fn(() => EDeviceType.Classic),
244
+ } as unknown as Device;
245
+
246
+ method.checkDeviceToBootloader(WEBUSB_ID);
247
+
248
+ for (let elapsed = 0; elapsed < 6500; elapsed += 500) {
249
+ jest.advanceTimersByTime(500);
250
+ // eslint-disable-next-line no-await-in-loop
251
+ await flush();
252
+ }
253
+
254
+ expect(probe).toHaveBeenCalled();
255
+ expect(maxInFlight).toBe(1);
256
+
257
+ resolveProbes[0]?.(false);
258
+ await flush();
259
+ jest.advanceTimersByTime(2000);
260
+ await flush();
261
+
262
+ expect(probe).toHaveBeenCalledTimes(2);
263
+ expect(maxInFlight).toBe(1);
264
+ resolveProbes[1]?.(false);
265
+ await flush();
266
+ });
267
+ });
268
+
206
269
  describe.each([
207
270
  [
208
271
  'FirmwareUpdateV2',
@@ -231,7 +294,7 @@ describe.each([
231
294
  jest.restoreAllMocks();
232
295
  });
233
296
 
234
- test('pauses the reboot deadline while waiting for browser authorization', async () => {
297
+ test('resumes bounded polling after browser authorization returns an empty enumeration', async () => {
235
298
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
236
299
  jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
237
300
  jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
@@ -300,13 +363,76 @@ describe.each([
300
363
  }
301
364
  expect(rejected).not.toHaveBeenCalled();
302
365
 
303
- permissionGranted = true;
304
366
  resolvePrompt('bootloader-device');
305
367
  await flush();
306
368
  await flush();
307
369
 
370
+ // The chooser may resolve before the newly authorized device appears in the
371
+ // next WebUSB enumeration. That transient empty result must not strand the
372
+ // deferred check after its original interval and timeout were cleared.
373
+ expect(resolved).not.toHaveBeenCalled();
374
+ expect(rejected).not.toHaveBeenCalled();
375
+
376
+ permissionGranted = true;
377
+ for (let elapsed = 0; elapsed < 5000; elapsed += 500) {
378
+ jest.advanceTimersByTime(500);
379
+ // eslint-disable-next-line no-await-in-loop
380
+ await flush();
381
+ if (resolved.mock.calls.length) break;
382
+ }
383
+
308
384
  expect(resolved).toHaveBeenCalledWith(true);
309
385
  expect(updateFromCache).toHaveBeenCalledWith(bootloaderDevice);
310
386
  expect(commands.disposed).toBe(false);
311
387
  });
388
+
389
+ test('rejects after the fresh deadline when the authorized device never appears', async () => {
390
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
391
+ jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
392
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
393
+ jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({ devices: {}, deviceList: [] });
394
+
395
+ const method = buildMethod();
396
+ method.device = {
397
+ originalDescriptor: {
398
+ id: WEBUSB_ID,
399
+ path: WEBUSB_ID,
400
+ protocolType: 'V1',
401
+ },
402
+ deviceConnector: {
403
+ enumerate: jest.fn().mockResolvedValue({ descriptors: [] }),
404
+ },
405
+ commands: { disposed: true },
406
+ updateFromCache: jest.fn(),
407
+ getCurrentDeviceType: jest.fn(() => EDeviceType.Pro),
408
+ } as unknown as Device;
409
+ method.postTipMessage = jest.fn();
410
+ const prompt = jest
411
+ .spyOn(method as any, '_promptDeviceInBootloaderForWebDevice')
412
+ .mockResolvedValue('bootloader-device');
413
+
414
+ method.checkDeviceToBootloader(WEBUSB_ID);
415
+ const resolved = jest.fn();
416
+ const rejected = jest.fn();
417
+ method.checkPromise?.promise.then(resolved, rejected);
418
+
419
+ for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
420
+ jest.advanceTimersByTime(500);
421
+ // eslint-disable-next-line no-await-in-loop
422
+ await flush();
423
+ if (prompt.mock.calls.length) break;
424
+ }
425
+ expect(prompt).toHaveBeenCalledTimes(1);
426
+
427
+ for (let elapsed = 0; elapsed < 31000; elapsed += 500) {
428
+ jest.advanceTimersByTime(500);
429
+ // eslint-disable-next-line no-await-in-loop
430
+ await flush();
431
+ if (rejected.mock.calls.length) break;
432
+ }
433
+
434
+ expect(resolved).not.toHaveBeenCalled();
435
+ expect(rejected).toHaveBeenCalledTimes(1);
436
+ expect(prompt).toHaveBeenCalledTimes(1);
437
+ });
312
438
  });
@@ -1,6 +1,10 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+ import {
5
+ registerFirmwareUpdateHostBinding,
6
+ unregisterFirmwareUpdateHostBinding,
7
+ } from '../../src/api/firmware/FirmwareHostBinding';
4
8
  import {
5
9
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
6
10
  prepareFirmwareUpdatePlan,
@@ -179,6 +183,19 @@ describe('firmware update plan identity in bootloader mode', () => {
179
183
  ],
180
184
  });
181
185
  };
186
+ const createBinding = (preparedPlanDigest: string) => ({
187
+ preparedPlanDigest,
188
+ artifactReader: {
189
+ open: () => Promise.resolve({ readerId: 'unused', size: 0 }),
190
+ read: () =>
191
+ Promise.resolve({
192
+ data: new ArrayBuffer(0),
193
+ bytesRead: 0,
194
+ eof: true,
195
+ }),
196
+ close: () => Promise.resolve(),
197
+ },
198
+ });
182
199
 
183
200
  test('accepts the phase that runs after the device reboots and reports a serial', () => {
184
201
  const preparedPlan = prepareRecoveryPlan();
@@ -250,6 +267,69 @@ describe('firmware update plan identity in bootloader mode', () => {
250
267
  })
251
268
  ).not.toThrow();
252
269
  });
270
+
271
+ test('releases the identity pin when the digest-bound host workflow ends', () => {
272
+ const preparedPlan = prepareRecoveryPlan();
273
+ const firstGeneration = registerFirmwareUpdateHostBinding(
274
+ createBinding(preparedPlan.preparedPlanDigest)
275
+ );
276
+ try {
277
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
278
+ preparedPlan,
279
+ deviceIdentity: 'CLA45F0023',
280
+ bootloaderMode: false,
281
+ deviceModel: String(EDeviceType.Classic1s),
282
+ });
283
+ } finally {
284
+ expect(unregisterFirmwareUpdateHostBinding(firstGeneration)).toBe(true);
285
+ }
286
+
287
+ const secondGeneration = registerFirmwareUpdateHostBinding(
288
+ createBinding(preparedPlan.preparedPlanDigest)
289
+ );
290
+ try {
291
+ expect(() =>
292
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
293
+ preparedPlan,
294
+ deviceIdentity: 'CLA99Z9999',
295
+ bootloaderMode: false,
296
+ deviceModel: String(EDeviceType.Classic1s),
297
+ })
298
+ ).not.toThrow();
299
+ } finally {
300
+ expect(unregisterFirmwareUpdateHostBinding(secondGeneration)).toBe(true);
301
+ }
302
+ });
303
+
304
+ test('a stale unregister cannot release the active workflow identity pin', () => {
305
+ const stalePlan = prepareRecoveryPlan();
306
+ const staleGeneration = registerFirmwareUpdateHostBinding(
307
+ createBinding(stalePlan.preparedPlanDigest)
308
+ );
309
+ const activePlan = prepareRecoveryPlan();
310
+ const activeGeneration = registerFirmwareUpdateHostBinding(
311
+ createBinding(activePlan.preparedPlanDigest)
312
+ );
313
+ try {
314
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
315
+ preparedPlan: activePlan,
316
+ deviceIdentity: 'CLA45F0023',
317
+ bootloaderMode: false,
318
+ deviceModel: String(EDeviceType.Classic1s),
319
+ });
320
+ expect(unregisterFirmwareUpdateHostBinding(staleGeneration)).toBe(false);
321
+ expect(() =>
322
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
323
+ preparedPlan: activePlan,
324
+ deviceIdentity: 'CLA99Z9999',
325
+ bootloaderMode: false,
326
+ deviceModel: String(EDeviceType.Classic1s),
327
+ })
328
+ ).toThrow();
329
+ } finally {
330
+ expect(unregisterFirmwareUpdateHostBinding(activeGeneration)).toBe(true);
331
+ }
332
+ });
253
333
  });
254
334
 
255
335
  describe('prepared plan device identity assertion', () => {
@@ -1,6 +1,9 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
3
+ import {
4
+ buildFirmwareUpdatePlan,
5
+ validateProtocolV2FirmwareUpdateTargets,
6
+ } from '../../src/api/firmware/FirmwareUpdatePlan';
4
7
  import {
5
8
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
6
9
  prepareFirmwareUpdatePlan,
@@ -88,6 +91,18 @@ const createProtocolV2ForceInput = (
88
91
  forceUpdateTargets: forceUpdateTargets as BuildPlanInput['forceUpdateTargets'],
89
92
  });
90
93
 
94
+ describe('validateProtocolV2FirmwareUpdateTargets', () => {
95
+ test('normalizes the deprecated boot_resources alias to resource', () => {
96
+ expect(validateProtocolV2FirmwareUpdateTargets(['boot_resources'])).toEqual(['resource']);
97
+ });
98
+
99
+ test('rejects duplicate semantic targets after alias normalization', () => {
100
+ expectFirmwarePlanInvalid(() =>
101
+ validateProtocolV2FirmwareUpdateTargets(['boot_resources', 'resource'])
102
+ );
103
+ });
104
+ });
105
+
91
106
  describe('buildFirmwareUpdatePlan', () => {
92
107
  test('selects all legacy artifacts before execution and chooses the desktop full resource', () => {
93
108
  const plan = buildFirmwareUpdatePlan({
@@ -316,65 +331,114 @@ describe('buildFirmwareUpdatePlan', () => {
316
331
  expectFirmwarePlanInvalid(() => buildFirmwareUpdatePlan(createLegacyForceInput(['resource'])));
317
332
  });
318
333
 
319
- test('rejects a legacy resource archive without complete integrity metadata', () => {
320
- expectFirmwarePlanInvalid(
321
- () =>
322
- buildFirmwareUpdatePlan(
323
- createLegacyForceInput(['resource'], {
324
- url: 'https://firmware.onekey.so/pro/firmware.bin',
325
- resource: 'https://firmware.onekey.so/pro/resource.zip',
326
- resourceExpectedSize: 4096,
327
- })
328
- ),
329
- 'integrity metadata is invalid'
330
- );
331
- });
332
-
333
- test.each(['firmware', 'ble', 'bootloader'] as const)(
334
- 'rejects a remote legacy %s artifact without complete integrity metadata',
335
- target => {
334
+ test.each([
335
+ {
336
+ executor: 'V2',
337
+ deviceType: EDeviceType.Classic1s,
338
+ bootloaderVersion: '2.1.2',
339
+ },
340
+ {
341
+ executor: 'V3',
342
+ deviceType: EDeviceType.Pro,
343
+ bootloaderVersion: '2.8.4',
344
+ },
345
+ ])(
346
+ 'rejects a resource-only prepared plan that the $executor executor cannot run',
347
+ ({ deviceType, bootloaderVersion }) => {
336
348
  expectFirmwarePlanInvalid(
337
349
  () =>
338
350
  buildFirmwareUpdatePlan({
339
- features: createFeatures({ deviceType: EDeviceType.Classic1s }),
351
+ features: createFeatures({
352
+ deviceType,
353
+ firmwareVersion: '4.21.0',
354
+ bootloaderVersion,
355
+ }),
340
356
  firmwareType: EFirmwareType.Universal,
341
357
  platform: 'desktop',
342
- firmware:
343
- target === 'firmware'
344
- ? {
345
- status: 'outdated',
346
- release: {
347
- url: 'https://firmware.onekey.so/classic/firmware.bin',
348
- expectedSize: 1024,
349
- },
350
- }
351
- : noUpdate,
352
- ble:
353
- target === 'ble'
354
- ? {
355
- status: 'outdated',
356
- release: {
357
- webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
358
- expectedSize: 512,
359
- },
360
- }
361
- : noUpdate,
362
- bootloader:
363
- target === 'bootloader'
364
- ? {
365
- status: 'outdated',
366
- release: {
367
- bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
368
- bootloaderExpectedSize: 768,
369
- },
370
- }
371
- : noUpdate,
358
+ firmware: {
359
+ status: 'valid',
360
+ release: {
361
+ url: 'https://firmware.onekey.so/legacy/firmware.bin',
362
+ version: [4, 21, 0],
363
+ expectedSize: 1024,
364
+ fingerprint: '1'.repeat(64),
365
+ resource: 'https://firmware.onekey.so/legacy/resource.zip',
366
+ resourceExpectedSize: 4096,
367
+ resourceFingerprint: '2'.repeat(64),
368
+ },
369
+ },
370
+ ble: noUpdate,
371
+ bootloader: noUpdate,
372
+ forceUpdateTargets: ['resource'],
372
373
  }),
373
- 'integrity metadata is invalid'
374
+ 'Legacy resource updates require a firmware target'
374
375
  );
375
376
  }
376
377
  );
377
378
 
379
+ test.each([
380
+ {
381
+ executor: 'v2',
382
+ deviceType: EDeviceType.Classic1s,
383
+ bootloaderVersion: '2.1.2',
384
+ },
385
+ {
386
+ executor: 'v3',
387
+ deviceType: EDeviceType.Pro,
388
+ bootloaderVersion: '2.8.4',
389
+ },
390
+ ])(
391
+ 'allows $executor config releases without optional integrity metadata',
392
+ ({ executor, deviceType, bootloaderVersion }) => {
393
+ const plan = buildFirmwareUpdatePlan({
394
+ features: createFeatures({
395
+ deviceType,
396
+ firmwareVersion: '4.21.0',
397
+ bootloaderVersion,
398
+ }),
399
+ firmwareType: EFirmwareType.Universal,
400
+ platform: 'desktop',
401
+ firmware: {
402
+ status: 'valid',
403
+ release: {
404
+ url: 'https://firmware.onekey.so/legacy/firmware.bin',
405
+ version: [4, 21, 0],
406
+ fingerprint: '',
407
+ resource: 'https://firmware.onekey.so/legacy/resource.zip',
408
+ resourceFingerprint: '',
409
+ },
410
+ },
411
+ ble: {
412
+ status: 'valid',
413
+ release: {
414
+ webUpdate: 'https://firmware.onekey.so/legacy/ble.bin',
415
+ version: [2, 3, 7],
416
+ fingerprintWeb: '',
417
+ },
418
+ },
419
+ bootloader: {
420
+ status: 'valid',
421
+ release: {
422
+ bootloaderResource: 'https://firmware.onekey.so/legacy/bootloader.bin',
423
+ bootloaderVersion: [2, 8, 4],
424
+ bootloaderFingerprint: '',
425
+ },
426
+ },
427
+ forceUpdateTargets: ['firmware', 'resource', 'ble', 'bootloader'],
428
+ });
429
+
430
+ expect(plan.executor).toBe(executor);
431
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual([
432
+ 'bootloader',
433
+ 'firmware',
434
+ 'resource',
435
+ 'ble',
436
+ ]);
437
+ expect(plan.artifacts.every(artifact => artifact.expectedSize === undefined)).toBe(true);
438
+ expect(plan.artifacts.every(artifact => artifact.expectedSha256 === undefined)).toBe(true);
439
+ }
440
+ );
441
+
378
442
  test.each(['ble', 'bootloader'] as const)(
379
443
  'rejects the unsupported Pro2 %s force target',
380
444
  forceTarget => {
@@ -179,7 +179,7 @@ describe('FirmwareUpdatePreparedPlan', () => {
179
179
  ).toThrow('plan digest is invalid');
180
180
  });
181
181
 
182
- test('projects portable archive paths to unique device resource names', () => {
182
+ test('keeps ZIP entries unique by archive path, not basename', () => {
183
183
  const plan = buildPlan();
184
184
  const buildArtifacts = (entryNames: string[]) => [
185
185
  {
@@ -200,7 +200,17 @@ describe('FirmwareUpdatePreparedPlan', () => {
200
200
  prepareFirmwareUpdatePlan({
201
201
  plan,
202
202
  leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
203
- artifacts: buildArtifacts(['icons/index.bin', 'fonts/index.bin']),
203
+ artifacts: buildArtifacts([
204
+ 'loaders/bootloader/params-pro2-prod_resource-unsigned.okpkg',
205
+ 'loaders/rom/params-pro2-prod_resource-unsigned.okpkg',
206
+ ]),
207
+ })
208
+ ).not.toThrow();
209
+ expect(() =>
210
+ prepareFirmwareUpdatePlan({
211
+ plan,
212
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
213
+ artifacts: buildArtifacts(['loaders/rom/params.okpkg', 'loaders/rom/params.okpkg']),
204
214
  })
205
215
  ).toThrow('duplicate entry names');
206
216
  expect(() =>
@@ -234,7 +244,11 @@ describe('FirmwareUpdatePreparedPlan', () => {
234
244
  },
235
245
  ],
236
246
  });
237
- const resourceArtifact = prepared.artifacts.find(item => item.target === 'resource')!;
247
+ const resourceArtifact = prepared.artifacts.find(item => item.target === 'resource');
248
+ expect(resourceArtifact).toBeDefined();
249
+ if (!resourceArtifact) {
250
+ throw new Error('Expected prepared resource artifact');
251
+ }
238
252
  const tamperedWithoutDigest = {
239
253
  ...prepared,
240
254
  artifacts: prepared.artifacts.map(item =>
@@ -242,9 +256,9 @@ describe('FirmwareUpdatePreparedPlan', () => {
242
256
  ? {
243
257
  ...item,
244
258
  materializedEntries: [
245
- ...resourceArtifact.materializedEntries!,
259
+ ...(resourceArtifact.materializedEntries ?? []),
246
260
  {
247
- entryName: 'other/index.bin',
261
+ entryName: 'resource/index.bin',
248
262
  artifact: artifact('d'.repeat(64), 2),
249
263
  },
250
264
  ],
@@ -1,6 +1,7 @@
1
1
  import * as hdShared from '@onekeyfe/hd-shared';
2
2
 
3
3
  import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
4
+ import { DataManager } from '../../src/data-manager';
4
5
  import { DevicePool } from '../../src/device/DevicePool';
5
6
 
6
7
  import type { Device } from '../../src/device/Device';
@@ -112,10 +113,125 @@ describe('FirmwareUpdateV3 reconnect', () => {
112
113
  })
113
114
  ).rejects.toBe(reconnectError);
114
115
 
116
+ expect(commands.typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
115
117
  expect(waitForDeviceReconnect).toHaveBeenCalledWith(3 * 60 * 1000);
116
118
  } finally {
117
119
  jest.clearAllTimers();
118
120
  jest.useRealTimers();
119
121
  }
120
122
  });
123
+
124
+ it('lets Browser WebUSB clean up a timed out install probe before reconnecting', async () => {
125
+ jest.spyOn(hdShared, 'wait').mockResolvedValue(undefined);
126
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
127
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
128
+ const method = new FirmwareUpdateV3({
129
+ id: 1,
130
+ payload: {
131
+ method: 'firmwareUpdateV3',
132
+ connectId: 'webusb-device',
133
+ },
134
+ });
135
+ const reconnectError = new Error('stop after WebUSB reconnect assertion');
136
+ const waitForDeviceReconnect = jest
137
+ .spyOn(method, 'waitForDeviceReconnect')
138
+ .mockRejectedValue(reconnectError);
139
+ method.isBleReconnect = jest.fn(() => false);
140
+ method.params = {
141
+ platform: 'web',
142
+ } as any;
143
+ const commands = {
144
+ typedCall: jest.fn().mockRejectedValue(new Error('Protocol V1 read timeout after 3000ms')),
145
+ };
146
+ method.device = {
147
+ features: {},
148
+ getCommands: () => commands,
149
+ } as unknown as Device;
150
+ (method as any).createUpdatesFolderIfNotExists = jest.fn().mockResolvedValue(undefined);
151
+ (method as any).startEmmcFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
152
+ method.postTipMessage = jest.fn();
153
+ method.postProcessingMessage = jest.fn();
154
+ method.postProgressMessage = jest.fn();
155
+
156
+ await expect(
157
+ (method as any).executeUpdate({
158
+ resourceBinary: null,
159
+ resourceEntries: [],
160
+ fwSources: [],
161
+ bootloaderSource: null,
162
+ })
163
+ ).rejects.toBe(reconnectError);
164
+
165
+ expect(commands.typedCall).toHaveBeenCalledWith(
166
+ 'GetFeatures',
167
+ 'Features',
168
+ {},
169
+ {
170
+ timeoutMs: 3000,
171
+ }
172
+ );
173
+ expect(waitForDeviceReconnect).toHaveBeenCalledWith(60 * 1000);
174
+ });
175
+
176
+ it('does not treat cached firmware versions as Browser WebUSB install completion', async () => {
177
+ jest.spyOn(hdShared, 'wait').mockResolvedValue(undefined);
178
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
179
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
180
+ const method = new FirmwareUpdateV3({
181
+ id: 1,
182
+ payload: {
183
+ method: 'firmwareUpdateV3',
184
+ connectId: 'webusb-device',
185
+ },
186
+ });
187
+ method.params = {
188
+ platform: 'web',
189
+ } as any;
190
+ const commands = {
191
+ typedCall: jest
192
+ .fn()
193
+ .mockResolvedValueOnce({
194
+ type: 'Features',
195
+ message: {
196
+ major_version: 0,
197
+ minor_version: 0,
198
+ patch_version: 0,
199
+ },
200
+ })
201
+ .mockResolvedValueOnce({
202
+ type: 'Features',
203
+ message: {
204
+ major_version: 4,
205
+ minor_version: 21,
206
+ patch_version: 0,
207
+ onekey_ble_version: '2.3.7',
208
+ },
209
+ }),
210
+ };
211
+ method.device = {
212
+ features: {
213
+ firmwareVersion: '4.21.0',
214
+ bleVersion: '2.3.7',
215
+ },
216
+ getCommands: () => commands,
217
+ } as unknown as Device;
218
+ (method as any).createUpdatesFolderIfNotExists = jest.fn().mockResolvedValue(undefined);
219
+ (method as any).startEmmcFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
220
+ method.postTipMessage = jest.fn();
221
+ method.postProcessingMessage = jest.fn();
222
+ method.postProgressMessage = jest.fn();
223
+
224
+ const result = await (method as any).executeUpdate({
225
+ resourceBinary: null,
226
+ resourceEntries: [],
227
+ fwSources: [],
228
+ bootloaderSource: null,
229
+ });
230
+
231
+ expect(commands.typedCall).toHaveBeenCalledTimes(2);
232
+ expect(result).toMatchObject({
233
+ firmwareVersion: '4.21.0',
234
+ bleVersion: '2.3.7',
235
+ });
236
+ });
121
237
  });