@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.23

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 (192) hide show
  1. package/__tests__/DeviceCommands.test.ts +85 -0
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +2 -2
  5. package/__tests__/device-wallet-session-store.test.ts +152 -7
  6. package/__tests__/firmware-update/check-all-firmware-release.test.ts +86 -0
  7. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  8. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
  12. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  13. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  14. package/__tests__/logBlockEvent.test.ts +17 -0
  15. package/__tests__/open-wallet-session.test.ts +311 -12
  16. package/__tests__/pro2Wallpaper.test.ts +7 -18
  17. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  18. package/__tests__/protocol-v2.test.ts +685 -177
  19. package/__tests__/public-device-state-api.test.ts +69 -62
  20. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  21. package/dist/api/BaseMethod.d.ts +2 -1
  22. package/dist/api/BaseMethod.d.ts.map +1 -1
  23. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  24. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  27. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  29. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  30. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  31. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  32. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  33. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  34. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  35. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  36. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  37. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  38. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  39. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  40. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  41. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  42. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  43. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  44. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  45. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  46. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  47. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  48. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  49. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  50. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  52. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  53. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  54. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  55. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +23 -0
  56. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  57. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +30 -0
  58. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  59. package/dist/api/firmware/getBinary.d.ts +7 -0
  60. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  61. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  62. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  63. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  64. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  65. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  66. package/dist/api/index.d.ts +0 -17
  67. package/dist/api/index.d.ts.map +1 -1
  68. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  69. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  70. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/data-manager/DataManager.d.ts +1 -0
  73. package/dist/data-manager/DataManager.d.ts.map +1 -1
  74. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  75. package/dist/device/Device.d.ts +3 -2
  76. package/dist/device/Device.d.ts.map +1 -1
  77. package/dist/device/DeviceCommands.d.ts.map +1 -1
  78. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  79. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  80. package/dist/events/device.d.ts +1 -0
  81. package/dist/events/device.d.ts.map +1 -1
  82. package/dist/events/logBlockEvent.d.ts.map +1 -1
  83. package/dist/events/ui-request.d.ts +1 -0
  84. package/dist/events/ui-request.d.ts.map +1 -1
  85. package/dist/index.d.ts +273 -230
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3060 -1676
  88. package/dist/inject.d.ts.map +1 -1
  89. package/dist/lowLevelInject.d.ts.map +1 -1
  90. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  91. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  92. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  93. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  94. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  95. package/dist/topLevelInject.d.ts.map +1 -1
  96. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  97. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  98. package/dist/types/api/deviceUnlock.d.ts +2 -2
  99. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  100. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  101. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  102. package/dist/types/api/export.d.ts +4 -1
  103. package/dist/types/api/export.d.ts.map +1 -1
  104. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  105. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  106. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  107. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  108. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  109. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  110. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  111. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  112. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  113. package/dist/types/api/index.d.ts +10 -19
  114. package/dist/types/api/index.d.ts.map +1 -1
  115. package/dist/types/api/openWalletSession.d.ts +1 -0
  116. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  117. package/dist/types/api/protocolV2.d.ts +3 -76
  118. package/dist/types/api/protocolV2.d.ts.map +1 -1
  119. package/dist/types/api/sessionCache.d.ts +4 -1
  120. package/dist/types/api/sessionCache.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +13 -0
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  125. package/dist/utils/patch.d.ts +1 -1
  126. package/dist/utils/patch.d.ts.map +1 -1
  127. package/package.json +4 -4
  128. package/src/api/BaseMethod.ts +8 -3
  129. package/src/api/CheckAllFirmwareRelease.ts +23 -3
  130. package/src/api/ClearSessionCache.ts +4 -0
  131. package/src/api/FirmwareUpdate.ts +8 -1
  132. package/src/api/FirmwareUpdateV2.ts +292 -120
  133. package/src/api/FirmwareUpdateV3.ts +256 -55
  134. package/src/api/FirmwareUpdateV4.ts +821 -331
  135. package/src/api/GetPassphraseState.ts +11 -5
  136. package/src/api/OpenWalletSession.ts +74 -25
  137. package/src/api/UploadPortfolio.ts +1 -1
  138. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  139. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  140. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  141. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  142. package/src/api/device/DeviceChangePin.ts +1 -1
  143. package/src/api/device/DeviceUnlock.ts +1 -2
  144. package/src/api/device/DeviceUpdateBootloader.ts +122 -6
  145. package/src/api/device/DeviceWipe.ts +3 -1
  146. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  147. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  148. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  149. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  150. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  151. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +392 -0
  152. package/src/api/firmware/updateBootloader.ts +19 -4
  153. package/src/api/firmware/uploadFirmware.ts +140 -22
  154. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  155. package/src/api/index.ts +0 -18
  156. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  157. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  158. package/src/core/index.ts +1 -0
  159. package/src/data/messages/messages-protocol-v2.json +35 -1
  160. package/src/data-manager/DataManager.ts +33 -33
  161. package/src/data-manager/connectSettings.ts +11 -0
  162. package/src/device/Device.ts +80 -67
  163. package/src/device/DeviceCommands.ts +76 -159
  164. package/src/device/DeviceStateMapper.ts +18 -1
  165. package/src/device/DeviceStateProjector.ts +7 -1
  166. package/src/events/device.ts +1 -0
  167. package/src/events/logBlockEvent.ts +1 -14
  168. package/src/events/ui-request.ts +1 -0
  169. package/src/index.ts +5 -0
  170. package/src/inject.ts +22 -25
  171. package/src/lowLevelInject.ts +5 -1
  172. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -3
  173. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  174. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  175. package/src/protocols/protocol-v2/walletSession.ts +86 -9
  176. package/src/topLevelInject.ts +5 -1
  177. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  178. package/src/types/api/deviceUnlock.ts +2 -5
  179. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  180. package/src/types/api/export.ts +18 -0
  181. package/src/types/api/firmwareUpdate.ts +76 -0
  182. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  183. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  184. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  185. package/src/types/api/getPassphraseState.ts +4 -3
  186. package/src/types/api/index.ts +14 -34
  187. package/src/types/api/openWalletSession.ts +2 -0
  188. package/src/types/api/protocolV2.ts +3 -156
  189. package/src/types/api/sessionCache.ts +9 -4
  190. package/src/types/settings.ts +13 -0
  191. package/src/utils/deviceFeaturesUtils.ts +5 -1
  192. package/src/utils/deviceInfoUtils.ts +3 -16
@@ -41,6 +41,68 @@ describe('DeviceCommands failure mapping', () => {
41
41
  expect(JSON.stringify(log.messages)).not.toContain('secret-wallet-address');
42
42
  });
43
43
 
44
+ it('maps an invalid DeviceSessionGet resume to WalletSessionInvalid', async () => {
45
+ const commands = createCommands();
46
+
47
+ await expect(
48
+ commands._filterCommonTypes(
49
+ {
50
+ type: 'Failure',
51
+ message: {
52
+ code: 'Failure_ProcessError',
53
+ subcode: 2,
54
+ message: 'Invalid session',
55
+ },
56
+ } as any,
57
+ 'DeviceSessionGet'
58
+ )
59
+ ).rejects.toMatchObject({
60
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
61
+ params: {
62
+ failureCode: 'Failure_ProcessError',
63
+ subcode: 2,
64
+ firmwareMessage: 'Invalid session',
65
+ },
66
+ });
67
+ });
68
+
69
+ it('maps wallet-session user cancellation to ActionCancelled', async () => {
70
+ const commands = createCommands();
71
+
72
+ await expect(
73
+ commands._filterCommonTypes(
74
+ {
75
+ type: 'Failure',
76
+ message: {
77
+ code: 'Failure_ProcessError',
78
+ subcode: 1,
79
+ message: 'Cancelled on device',
80
+ },
81
+ } as any,
82
+ 'DeviceSessionAskPassphrase'
83
+ )
84
+ ).rejects.toMatchObject({
85
+ errorCode: HardwareErrorCode.ActionCancelled,
86
+ });
87
+ });
88
+
89
+ it.each([
90
+ [3, HardwareErrorCode.DeviceCheckUnlockTypeError],
91
+ [4, HardwareErrorCode.DeviceNotOpenedPassphrase],
92
+ ])('maps wallet-session subcode %s to its canonical wallet error', async (subcode, code) => {
93
+ const commands = createCommands();
94
+
95
+ await expect(
96
+ commands._filterCommonTypes(
97
+ {
98
+ type: 'Failure',
99
+ message: { code: 'Failure_ProcessError', subcode, message: 'Session selection failed' },
100
+ } as any,
101
+ 'DeviceSessionAskPin'
102
+ )
103
+ ).rejects.toMatchObject({ errorCode: code });
104
+ });
105
+
44
106
  it('rejects the Pro2 bootloader DeviceStatusGet unsupported response', async () => {
45
107
  const commands = createCommands();
46
108
 
@@ -382,3 +444,26 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
382
444
  });
383
445
  });
384
446
  });
447
+
448
+ describe('DeviceCommands cancellation', () => {
449
+ it('stops waiting after the bounded cancellation timeout', async () => {
450
+ jest.useFakeTimers({ doNotFake: ['performance'] });
451
+ try {
452
+ const commands = createCommands();
453
+ commands.disposed = false;
454
+ commands.callPromise = new Promise(() => {});
455
+ const dispose = jest.fn().mockResolvedValue(undefined);
456
+ commands.dispose = dispose;
457
+
458
+ const cancellation = commands.cancel();
459
+ jest.advanceTimersByTime(10_000);
460
+ await Promise.resolve();
461
+
462
+ await expect(cancellation).resolves.toBeUndefined();
463
+ expect(dispose).toHaveBeenCalledWith(true);
464
+ expect(commands.callPromise).toBeUndefined();
465
+ } finally {
466
+ jest.useRealTimers();
467
+ }
468
+ });
469
+ });
@@ -1,19 +1,61 @@
1
+ import DataManager from '../src/data-manager/DataManager';
1
2
  import { parseConnectSettings } from '../src/data-manager/connectSettings';
2
3
 
4
+ import type { RemoteConfigResponse } from '../src/types';
5
+
6
+ jest.mock('axios', () => ({
7
+ get: jest.fn(),
8
+ }));
9
+
3
10
  jest.mock('../src/data/config', () => ({
4
11
  getSDKVersion: jest.fn(() => '1.0.0'),
5
12
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
6
13
  }));
7
14
 
15
+ const mockAxiosGet: jest.Mock = jest.requireMock('axios').get;
16
+
8
17
  describe('parseConnectSettings', () => {
9
- it('keeps custom configFetcher in parsed settings', () => {
10
- const configFetcher = jest.fn();
18
+ const preloadedConfig = {
19
+ bridge: {},
20
+ classic: { firmware: [], ble: [] },
21
+ classic1s: { firmware: [], ble: [] },
22
+ classicpure: { firmware: [], ble: [] },
23
+ mini: { firmware: [], ble: [] },
24
+ touch: { firmware: [], ble: [] },
25
+ pro: { firmware: [], ble: [] },
26
+ pro2: { firmware: [], ble: [] },
27
+ } as unknown as RemoteConfigResponse;
28
+
29
+ it('keeps a serializable preloaded config in parsed settings', () => {
30
+ const settings = parseConnectSettings({
31
+ firmwareManifestMode: 'sdk-managed',
32
+ preloadedConfig,
33
+ });
34
+
35
+ expect(settings.firmwareManifestMode).toBe('sdk-managed');
36
+ expect(settings.preloadedConfig).toBe(preloadedConfig);
37
+ });
38
+
39
+ it('uses the preloaded config before the SDK network client', async () => {
40
+ const settings = parseConnectSettings({
41
+ firmwareManifestMode: 'sdk-managed',
42
+ preloadedConfig,
43
+ });
44
+
45
+ await DataManager.load(settings);
46
+
47
+ expect(mockAxiosGet).not.toHaveBeenCalled();
48
+ expect(DataManager.deviceMap.classic).toEqual({ firmware: [], ble: [] });
49
+ });
11
50
 
51
+ it('fails closed without a preloaded config in external-only mode', async () => {
12
52
  const settings = parseConnectSettings({
13
- fetchConfig: true,
14
- configFetcher,
53
+ firmwareManifestMode: 'external-only',
15
54
  });
16
55
 
17
- expect(settings.configFetcher).toBe(configFetcher);
56
+ await expect(DataManager.load(settings)).rejects.toThrow(
57
+ 'External firmware config snapshot is required'
58
+ );
59
+ expect(mockAxiosGet).not.toHaveBeenCalled();
18
60
  });
19
61
  });
@@ -1,13 +1,35 @@
1
1
  import { checkLiveDeviceId } from '../src/device/deviceIdentity';
2
+ import ConfluxGetAddress from '../src/api/conflux/ConfluxGetAddress';
3
+ import ConfluxSignMessage from '../src/api/conflux/ConfluxSignMessage';
4
+ import ConfluxSignMessageCIP23 from '../src/api/conflux/ConfluxSignMessageCIP23';
5
+ import ConfluxSignTransaction from '../src/api/conflux/ConfluxSignTransaction';
6
+ import NexaSignTransaction from '../src/api/nexa/NexaSignTransaction';
7
+
8
+ jest.mock('../src/data/config', () => ({
9
+ getSDKVersion: jest.fn(() => '1.0.0'),
10
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
11
+ }));
2
12
 
3
13
  describe('live device identity', () => {
14
+ test.each([
15
+ ['confluxGetAddress', ConfluxGetAddress],
16
+ ['confluxSignMessage', ConfluxSignMessage],
17
+ ['confluxSignMessageCIP23', ConfluxSignMessageCIP23],
18
+ ['confluxSignTransaction', ConfluxSignTransaction],
19
+ ['nexaSignTransaction', NexaSignTransaction],
20
+ ])('%s requires a live device id check', (methodName, Method) => {
21
+ const method = new Method({ payload: { method: methodName } });
22
+
23
+ expect(method.checkDeviceId).toBe(true);
24
+ });
25
+
4
26
  test('refreshes Protocol V2 status before comparing the expected device id', async () => {
5
27
  let currentDeviceId = 'OLD_DEVICE_ID';
6
28
  const device = {
7
29
  isProtocolV2: () => true,
8
- getDeviceState: jest.fn().mockImplementation(async () => {
30
+ getDeviceState: jest.fn().mockImplementation(() => {
9
31
  currentDeviceId = 'NEW_DEVICE_ID';
10
- return { identity: { deviceId: currentDeviceId } };
32
+ return Promise.resolve({ identity: { deviceId: currentDeviceId } });
11
33
  }),
12
34
  checkDeviceId: jest.fn((expected: string) => currentDeviceId === expected),
13
35
  };
@@ -37,7 +37,6 @@ function createDevice({ protocol }: { protocol: 'V1' | 'V2' }) {
37
37
 
38
38
  describe('DeviceSettings protocol routing', () => {
39
39
  it.each([
40
- ['label', { label: 'Shared Label' }],
41
40
  ['language', { language: 'ja-JP' }],
42
41
  ['brightness', { brightness: 80 }],
43
42
  ['haptic feedback', { hapticFeedback: true }],
@@ -56,6 +55,7 @@ describe('DeviceSettings protocol routing', () => {
56
55
  });
57
56
 
58
57
  it.each([
58
+ ['label', { label: 'Shared Label' }],
59
59
  ['auto lock', { autoLockDelayMs: 60_000 }],
60
60
  ['auto shutdown', { autoShutdownDelayMs: 120_000 }],
61
61
  ['mixed settings', { brightness: 80, autoLockDelayMs: 60_000 }],
@@ -70,7 +70,7 @@ describe('DeviceSettings protocol routing', () => {
70
70
 
71
71
  method.init();
72
72
 
73
- expect(method.unlockPolicy).toBe('retry-on-locked');
73
+ expect(method.unlockPolicy).toBe('unlock-before-run');
74
74
  });
75
75
 
76
76
  it('uses ApplySettings and commits the confirmed patch for Protocol V1', async () => {
@@ -1,10 +1,14 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
1
3
  import ClearSessionCache from '../src/api/ClearSessionCache';
4
+ import DeviceWipe from '../src/api/device/DeviceWipe';
2
5
  import {
3
6
  DeviceWalletSessionStore,
4
7
  deviceWalletSessionStore,
5
8
  } from '../src/device/DeviceWalletSessionStore';
6
9
  import { Device } from '../src/device/Device';
7
10
  import { createCoreApi } from '../src/inject';
11
+ import TransportManager from '../src/data-manager/TransportManager';
8
12
 
9
13
  jest.mock('../src/data/config', () => ({
10
14
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -93,6 +97,46 @@ describe('DeviceWalletSessionStore', () => {
93
97
  });
94
98
  });
95
99
 
100
+ describe('Device wipe lifecycle invalidation', () => {
101
+ beforeEach(() => {
102
+ deviceWalletSessionStore.clear();
103
+ });
104
+
105
+ test('clears the old Protocol V2 identity and wallet sessions after wipe succeeds', async () => {
106
+ const device = Device.fromDescriptor({
107
+ id: 'pro2-descriptor',
108
+ path: 'pro2-path',
109
+ protocolType: 'V2',
110
+ } as never);
111
+ device.features = {
112
+ protocol: 'V2',
113
+ deviceId: 'old-device-id',
114
+ unlocked: true,
115
+ passphraseProtection: true,
116
+ } as never;
117
+ device.passphraseState = 'hidden-a';
118
+ deviceWalletSessionStore.set('old-device-id', 'hidden-a', 'old-session');
119
+ deviceWalletSessionStore.set('pro2-path', 'hidden-a', 'temporary-session');
120
+ device.markPreInitialized({ passphraseState: 'hidden-a' });
121
+ device.commands = {
122
+ typedCall: jest.fn().mockResolvedValue({ message: { message: 'accepted' } }),
123
+ } as never;
124
+
125
+ const method = new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } });
126
+ method.init();
127
+ (method as any).device = device;
128
+
129
+ await expect(method.run()).resolves.toEqual({ message: 'accepted' });
130
+
131
+ expect(device.features).toBeUndefined();
132
+ expect(device.passphraseState).toBeUndefined();
133
+ expect(device.needReloadDevice).toBe(true);
134
+ expect(device.isPreInitializedValid(60_000)).toBe(false);
135
+ expect(deviceWalletSessionStore.get('old-device-id', 'hidden-a')).toBeUndefined();
136
+ expect(deviceWalletSessionStore.get('pro2-path', 'hidden-a')).toBeUndefined();
137
+ });
138
+ });
139
+
96
140
  describe('ClearSessionCache', () => {
97
141
  beforeEach(() => {
98
142
  deviceWalletSessionStore.clear();
@@ -116,7 +160,19 @@ describe('ClearSessionCache', () => {
116
160
  expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b');
117
161
  });
118
162
 
119
- test('keeps legacy Features session fields as projections of the store', async () => {
163
+ test('rejects passphraseState without a deviceId instead of clearing every device', () => {
164
+ deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a');
165
+ deviceWalletSessionStore.set('device-2', 'hidden-a', 'session-b');
166
+ const method = new ClearSessionCache({
167
+ payload: { method: 'clearSessionCache', passphraseState: 'hidden-a' },
168
+ });
169
+
170
+ expect(() => method.init()).toThrow('Parameter [deviceId] is required with [passphraseState].');
171
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBe('session-a');
172
+ expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b');
173
+ });
174
+
175
+ test('keeps wallet sessions internal instead of projecting them through Features', async () => {
120
176
  const device = Device.fromDescriptor({ id: 'one', path: 'one' } as never);
121
177
  device.features = {
122
178
  protocol: 'V1',
@@ -133,11 +189,14 @@ describe('ClearSessionCache', () => {
133
189
  } as never;
134
190
 
135
191
  expect(device.state).not.toHaveProperty('session');
192
+ expect(device.getInternalState()).toBe('session-a');
136
193
  expect(device.features).toMatchObject({
137
- sessionId: 'session-a',
138
- session_id: 'session-a',
139
- passphraseState: 'hidden-a',
194
+ sessionId: null,
195
+ session_id: null,
140
196
  });
197
+ expect(device.features).not.toHaveProperty('passphraseState');
198
+ expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('sessionId');
199
+ expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('session_id');
141
200
 
142
201
  const method = new ClearSessionCache({
143
202
  payload: {
@@ -151,10 +210,20 @@ describe('ClearSessionCache', () => {
151
210
 
152
211
  expect(device.features?.sessionId).toBeNull();
153
212
  expect(device.features?.session_id).toBeNull();
154
- expect(device.state?.raw?.protocolV1Features).toMatchObject({
213
+ expect(device.state?.raw?.protocolV1Features).not.toHaveProperty('sessionId');
214
+ expect(device.state?.raw?.protocolV1Features).not.toHaveProperty('session_id');
215
+ });
216
+
217
+ test('uses only the transport path as the temporary Protocol V2 cache key', () => {
218
+ const device = Device.fromDescriptor({ id: 'descriptor-id' } as never);
219
+ device.features = {
220
+ protocol: 'V2',
221
+ passphraseState: 'hidden-a',
155
222
  sessionId: 'session-a',
156
- session_id: 'session-a',
157
- });
223
+ } as never;
224
+
225
+ expect(device.getInternalState()).toBeUndefined();
226
+ expect(deviceWalletSessionStore.get('descriptor-id', 'hidden-a')).toBeUndefined();
158
227
  });
159
228
 
160
229
  test('clears one wallet without using a device', async () => {
@@ -211,3 +280,79 @@ describe('ClearSessionCache', () => {
211
280
  });
212
281
  });
213
282
  });
283
+
284
+ describe('Protocol V1 wallet identity initialization', () => {
285
+ beforeEach(() => {
286
+ deviceWalletSessionStore.clear();
287
+ jest.restoreAllMocks();
288
+ });
289
+
290
+ test('verifies the live device id before sending a cached wallet session', async () => {
291
+ const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
292
+ device.features = {
293
+ protocol: 'V1',
294
+ deviceId: 'cached-device-a',
295
+ unlocked: true,
296
+ passphraseProtection: true,
297
+ } as never;
298
+ deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
299
+ const typedCall = jest.fn().mockResolvedValue({
300
+ type: 'Features',
301
+ message: { device_id: 'live-device-b' },
302
+ });
303
+ device.commands = { typedCall } as never;
304
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
305
+
306
+ await expect(
307
+ device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
308
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
309
+ expect(typedCall).toHaveBeenCalledTimes(1);
310
+ expect(typedCall).toHaveBeenCalledWith(
311
+ 'Initialize',
312
+ 'Features',
313
+ expect.not.objectContaining({ session_id: 'session-a' }),
314
+ expect.any(Object)
315
+ );
316
+ });
317
+
318
+ test('resumes a cached V1 wallet only after the live device id matches', async () => {
319
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
320
+ device.features = {
321
+ protocol: 'V1',
322
+ deviceId: 'device-a',
323
+ unlocked: true,
324
+ passphraseProtection: true,
325
+ } as never;
326
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
327
+ const typedCall = jest
328
+ .fn()
329
+ .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
330
+ .mockResolvedValueOnce({
331
+ type: 'Features',
332
+ message: { device_id: 'device-a', session_id: 'session-a' },
333
+ });
334
+ device.commands = { typedCall } as never;
335
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
336
+
337
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
338
+
339
+ expect(typedCall).toHaveBeenCalledTimes(2);
340
+ expect(typedCall).toHaveBeenNthCalledWith(
341
+ 1,
342
+ 'Initialize',
343
+ 'Features',
344
+ expect.not.objectContaining({ session_id: 'session-a' }),
345
+ expect.any(Object)
346
+ );
347
+ expect(typedCall).toHaveBeenNthCalledWith(
348
+ 2,
349
+ 'Initialize',
350
+ 'Features',
351
+ expect.objectContaining({
352
+ session_id: 'session-a',
353
+ passphrase_state: 'hidden-a',
354
+ }),
355
+ expect.any(Object)
356
+ );
357
+ });
358
+ });
@@ -0,0 +1,86 @@
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
+
3
+ import CheckAllFirmwareRelease from '../../src/api/CheckAllFirmwareRelease';
4
+ import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
5
+ import {
6
+ getBleFirmwareReleaseInfo,
7
+ getBootloaderReleaseInfo,
8
+ getFirmwareReleaseInfo,
9
+ } from '../../src/api/firmware/releaseHelper';
10
+
11
+ jest.mock('../../src/data/config', () => ({
12
+ getSDKVersion: jest.fn(() => '1.0.0'),
13
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
14
+ }));
15
+
16
+ jest.mock('../../src/api/firmware/FirmwareUpdatePlan', () => ({
17
+ buildFirmwareUpdatePlan: jest.fn(),
18
+ }));
19
+
20
+ jest.mock('../../src/api/firmware/releaseHelper', () => ({
21
+ getBleFirmwareReleaseInfo: jest.fn(),
22
+ getBootloaderReleaseInfo: jest.fn(),
23
+ getFirmwareReleaseInfo: jest.fn(),
24
+ }));
25
+
26
+ const mockBuildFirmwareUpdatePlan = buildFirmwareUpdatePlan as jest.MockedFunction<
27
+ typeof buildFirmwareUpdatePlan
28
+ >;
29
+ const mockGetBleFirmwareReleaseInfo = getBleFirmwareReleaseInfo as jest.MockedFunction<
30
+ typeof getBleFirmwareReleaseInfo
31
+ >;
32
+ const mockGetBootloaderReleaseInfo = getBootloaderReleaseInfo as jest.MockedFunction<
33
+ typeof getBootloaderReleaseInfo
34
+ >;
35
+ const mockGetFirmwareReleaseInfo = getFirmwareReleaseInfo as jest.MockedFunction<
36
+ typeof getFirmwareReleaseInfo
37
+ >;
38
+
39
+ describe('CheckAllFirmwareRelease', () => {
40
+ test('keeps release information available when an optional Plan cannot be built', async () => {
41
+ const firmware = {
42
+ status: 'outdated' as const,
43
+ release: {
44
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
45
+ version: [3, 0, 0],
46
+ },
47
+ };
48
+ const noUpdate = {
49
+ status: 'valid' as const,
50
+ release: undefined,
51
+ };
52
+ mockGetFirmwareReleaseInfo.mockReturnValue(firmware);
53
+ mockGetBootloaderReleaseInfo.mockReturnValue(noUpdate);
54
+ mockGetBleFirmwareReleaseInfo.mockReturnValue(noUpdate);
55
+ mockBuildFirmwareUpdatePlan.mockImplementation(() => {
56
+ throw new Error('FirmwarePlanInvalid');
57
+ });
58
+
59
+ const method = new CheckAllFirmwareRelease({
60
+ id: 1,
61
+ payload: {
62
+ method: 'checkAllFirmwareRelease',
63
+ platform: 'native',
64
+ },
65
+ });
66
+ method.init();
67
+ method.device = {
68
+ features: {
69
+ deviceType: EDeviceType.Classic1s,
70
+ serialNo: '',
71
+ firmwareType: EFirmwareType.Universal,
72
+ firmwareVersion: '1.0.0',
73
+ bootloaderVersion: '1.0.0',
74
+ },
75
+ } as typeof method.device;
76
+
77
+ await expect(method.run()).resolves.toEqual(
78
+ expect.objectContaining({
79
+ firmware,
80
+ bootloader: noUpdate,
81
+ ble: noUpdate,
82
+ firmwareUpdatePlan: undefined,
83
+ })
84
+ );
85
+ });
86
+ });
@@ -0,0 +1,119 @@
1
+ import {
2
+ MAX_FIRMWARE_ARTIFACT_READ_BYTES,
3
+ openFirmwareByteSource,
4
+ writeFirmwareByteSource,
5
+ } from '../../src/api/firmware/FirmwareArtifactSource';
6
+
7
+ import type { FirmwareArtifactReader } from '../../src/types/api/firmwareUpdate';
8
+
9
+ const artifact = {
10
+ artifactRef: `fw:${'a'.repeat(64)}`,
11
+ size: 6,
12
+ sha256: 'b'.repeat(64),
13
+ };
14
+
15
+ const createReader = () => {
16
+ const open = jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: artifact.size }));
17
+ const read = jest.fn(({ offset, length }: { readerId: string; offset: number; length: number }) =>
18
+ Promise.resolve({
19
+ data: Uint8Array.from([1, 2, 3, 4, 5, 6]).slice(offset, offset + length).buffer,
20
+ bytesRead: length,
21
+ eof: offset + length === artifact.size,
22
+ })
23
+ );
24
+ const close = jest.fn(() => Promise.resolve());
25
+ const reader: FirmwareArtifactReader = { open, read, close };
26
+ return { reader, open, read, close };
27
+ };
28
+
29
+ describe('FirmwareArtifactSource', () => {
30
+ it('reads an opaque artifact through bounded sequential reads', async () => {
31
+ const { reader, close } = createReader();
32
+ const source = await openFirmwareByteSource({ artifact, reader });
33
+ if (!source) {
34
+ throw new Error('Expected a firmware byte source');
35
+ }
36
+
37
+ const writes: Array<{ offset: number; data: number[] }> = [];
38
+ await writeFirmwareByteSource({
39
+ source,
40
+ chunkSize: 4,
41
+ write: ({ data, sourceOffset, length }) => {
42
+ writes.push({
43
+ offset: sourceOffset,
44
+ data: Array.from(new Uint8Array(data)),
45
+ });
46
+ return Promise.resolve(length);
47
+ },
48
+ });
49
+ await source.close();
50
+
51
+ expect(writes).toEqual([
52
+ { offset: 0, data: [1, 2, 3, 4] },
53
+ { offset: 4, data: [5, 6] },
54
+ ]);
55
+ expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
56
+ });
57
+
58
+ it('rejects a reader whose opened size differs from the receipt', async () => {
59
+ const { reader, open, close } = createReader();
60
+ open.mockResolvedValue({
61
+ readerId: 'reader-1',
62
+ size: artifact.size + 1,
63
+ });
64
+
65
+ await expect(openFirmwareByteSource({ artifact, reader })).rejects.toMatchObject({
66
+ params: {
67
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
68
+ },
69
+ });
70
+ expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
71
+ });
72
+
73
+ it('rejects paths and over-limit reads before invoking the host', async () => {
74
+ const { reader, read } = createReader();
75
+ await expect(
76
+ openFirmwareByteSource({
77
+ artifact: { ...artifact, artifactRef: '/tmp/firmware.bin' },
78
+ reader,
79
+ })
80
+ ).rejects.toMatchObject({
81
+ params: {
82
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
83
+ },
84
+ });
85
+
86
+ const source = await openFirmwareByteSource({ artifact, reader });
87
+ if (!source) {
88
+ throw new Error('Expected a firmware byte source');
89
+ }
90
+ await expect(
91
+ writeFirmwareByteSource({
92
+ source,
93
+ chunkSize: MAX_FIRMWARE_ARTIFACT_READ_BYTES + 1,
94
+ write: ({ length }) => Promise.resolve(length),
95
+ })
96
+ ).rejects.toMatchObject({
97
+ params: {
98
+ firmwareUpdateCode: 'FirmwareArtifactReadTooLarge',
99
+ },
100
+ });
101
+ expect(read).not.toHaveBeenCalled();
102
+ await source.close();
103
+ });
104
+
105
+ it('closes stale readers when open returns invalid metadata', async () => {
106
+ const { reader, open, close } = createReader();
107
+ open.mockResolvedValue({
108
+ readerId: '',
109
+ size: artifact.size,
110
+ });
111
+
112
+ await expect(openFirmwareByteSource({ artifact, reader })).rejects.toMatchObject({
113
+ params: {
114
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
115
+ },
116
+ });
117
+ expect(close).not.toHaveBeenCalled();
118
+ });
119
+ });