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

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 (175) hide show
  1. package/__tests__/DeviceCommands.test.ts +87 -2
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +1 -1
  5. package/__tests__/device-wallet-session-store.test.ts +111 -7
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  9. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  10. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +200 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  12. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  13. package/__tests__/open-wallet-session.test.ts +253 -13
  14. package/__tests__/pro2Wallpaper.test.ts +7 -18
  15. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  16. package/__tests__/protocol-v2.test.ts +415 -145
  17. package/__tests__/public-device-state-api.test.ts +69 -62
  18. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  19. package/dist/api/BaseMethod.d.ts +2 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  31. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  32. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  33. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  34. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  35. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  36. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  37. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  38. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  39. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  40. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  41. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  42. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  43. package/dist/api/firmware/FirmwareArtifactSource.d.ts +26 -0
  44. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  45. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  46. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  47. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  48. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  49. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  50. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +24 -0
  52. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  53. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +25 -0
  54. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  55. package/dist/api/firmware/getBinary.d.ts +7 -0
  56. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  57. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  58. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  59. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  60. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  61. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  62. package/dist/api/index.d.ts +0 -17
  63. package/dist/api/index.d.ts.map +1 -1
  64. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  65. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  66. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  67. package/dist/data-manager/DataManager.d.ts +1 -0
  68. package/dist/data-manager/DataManager.d.ts.map +1 -1
  69. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts.map +1 -1
  71. package/dist/device/DeviceCommands.d.ts.map +1 -1
  72. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  73. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  74. package/dist/events/logBlockEvent.d.ts.map +1 -1
  75. package/dist/index.d.ts +265 -226
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3045 -1550
  78. package/dist/inject.d.ts.map +1 -1
  79. package/dist/lowLevelInject.d.ts.map +1 -1
  80. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  81. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  82. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  83. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  84. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  85. package/dist/topLevelInject.d.ts.map +1 -1
  86. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  87. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  88. package/dist/types/api/deviceUnlock.d.ts +2 -2
  89. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  90. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  91. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  92. package/dist/types/api/export.d.ts +4 -1
  93. package/dist/types/api/export.d.ts.map +1 -1
  94. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  95. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  96. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  97. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  98. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  99. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  100. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  101. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  102. package/dist/types/api/index.d.ts +10 -19
  103. package/dist/types/api/index.d.ts.map +1 -1
  104. package/dist/types/api/openWalletSession.d.ts +1 -0
  105. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  106. package/dist/types/api/protocolV2.d.ts +3 -76
  107. package/dist/types/api/protocolV2.d.ts.map +1 -1
  108. package/dist/types/api/sessionCache.d.ts +4 -1
  109. package/dist/types/api/sessionCache.d.ts.map +1 -1
  110. package/dist/types/settings.d.ts +13 -0
  111. package/dist/types/settings.d.ts.map +1 -1
  112. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  113. package/dist/utils/patch.d.ts +1 -1
  114. package/dist/utils/patch.d.ts.map +1 -1
  115. package/package.json +4 -4
  116. package/src/api/BaseMethod.ts +8 -3
  117. package/src/api/CheckAllFirmwareRelease.ts +16 -3
  118. package/src/api/ClearSessionCache.ts +4 -0
  119. package/src/api/FirmwareUpdate.ts +8 -1
  120. package/src/api/FirmwareUpdateV2.ts +284 -119
  121. package/src/api/FirmwareUpdateV3.ts +248 -55
  122. package/src/api/FirmwareUpdateV4.ts +816 -328
  123. package/src/api/OpenWalletSession.ts +74 -25
  124. package/src/api/UploadPortfolio.ts +1 -1
  125. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  126. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  127. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  128. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  129. package/src/api/device/DeviceChangePin.ts +1 -1
  130. package/src/api/device/DeviceUnlock.ts +1 -2
  131. package/src/api/device/DeviceUpdateBootloader.ts +114 -6
  132. package/src/api/device/DeviceWipe.ts +1 -1
  133. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  134. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  135. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  136. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  137. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  138. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +378 -0
  139. package/src/api/firmware/updateBootloader.ts +19 -4
  140. package/src/api/firmware/uploadFirmware.ts +140 -22
  141. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  142. package/src/api/index.ts +0 -18
  143. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  144. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  145. package/src/data/messages/messages-protocol-v2.json +77 -4
  146. package/src/data-manager/DataManager.ts +33 -33
  147. package/src/data-manager/connectSettings.ts +11 -0
  148. package/src/device/Device.ts +50 -60
  149. package/src/device/DeviceCommands.ts +58 -14
  150. package/src/device/DeviceStateMapper.ts +18 -1
  151. package/src/device/DeviceStateProjector.ts +7 -1
  152. package/src/events/logBlockEvent.ts +1 -0
  153. package/src/index.ts +5 -0
  154. package/src/inject.ts +22 -25
  155. package/src/lowLevelInject.ts +5 -1
  156. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  157. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -2
  158. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  159. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  160. package/src/protocols/protocol-v2/walletSession.ts +68 -12
  161. package/src/topLevelInject.ts +5 -1
  162. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  163. package/src/types/api/deviceUnlock.ts +2 -5
  164. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  165. package/src/types/api/export.ts +18 -0
  166. package/src/types/api/firmwareUpdate.ts +76 -0
  167. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  168. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  169. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  170. package/src/types/api/index.ts +14 -34
  171. package/src/types/api/openWalletSession.ts +2 -0
  172. package/src/types/api/protocolV2.ts +3 -156
  173. package/src/types/api/sessionCache.ts +9 -4
  174. package/src/types/settings.ts +13 -0
  175. package/src/utils/deviceFeaturesUtils.ts +5 -1
@@ -19,7 +19,7 @@ const createCommands = () => {
19
19
  };
20
20
 
21
21
  describe('DeviceCommands failure mapping', () => {
22
- it('does not log DeviceSessionGet response secrets', async () => {
22
+ it('does not log DeviceSessionOpen response secrets', async () => {
23
23
  const commands = createCommands();
24
24
  const log = getLogger(LoggerNames.DeviceCommands);
25
25
  log.messages.length = 0;
@@ -33,7 +33,7 @@ describe('DeviceCommands failure mapping', () => {
33
33
  btc_test_address: 'secret-wallet-address',
34
34
  },
35
35
  } as any,
36
- 'DeviceSessionGet'
36
+ 'DeviceSessionOpen'
37
37
  )
38
38
  ).resolves.toMatchObject({ type: 'DeviceSession' });
39
39
 
@@ -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 DeviceSessionOpen 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
+ 'DeviceSessionOpen'
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 DeviceSessionOpen 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
+ 'DeviceSessionOpen'
83
+ )
84
+ ).rejects.toMatchObject({
85
+ errorCode: HardwareErrorCode.ActionCancelled,
86
+ });
87
+ });
88
+
89
+ it.each([
90
+ [3, HardwareErrorCode.DeviceCheckUnlockTypeError],
91
+ [4, HardwareErrorCode.DeviceNotOpenedPassphrase],
92
+ ])('maps DeviceSessionOpen 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
+ 'DeviceSessionOpen'
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
  };
@@ -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,3 +1,5 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
1
3
  import ClearSessionCache from '../src/api/ClearSessionCache';
2
4
  import {
3
5
  DeviceWalletSessionStore,
@@ -5,6 +7,7 @@ import {
5
7
  } from '../src/device/DeviceWalletSessionStore';
6
8
  import { Device } from '../src/device/Device';
7
9
  import { createCoreApi } from '../src/inject';
10
+ import TransportManager from '../src/data-manager/TransportManager';
8
11
 
9
12
  jest.mock('../src/data/config', () => ({
10
13
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -116,7 +119,19 @@ describe('ClearSessionCache', () => {
116
119
  expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b');
117
120
  });
118
121
 
119
- test('keeps legacy Features session fields as projections of the store', async () => {
122
+ test('rejects passphraseState without a deviceId instead of clearing every device', () => {
123
+ deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a');
124
+ deviceWalletSessionStore.set('device-2', 'hidden-a', 'session-b');
125
+ const method = new ClearSessionCache({
126
+ payload: { method: 'clearSessionCache', passphraseState: 'hidden-a' },
127
+ });
128
+
129
+ expect(() => method.init()).toThrow('Parameter [deviceId] is required with [passphraseState].');
130
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBe('session-a');
131
+ expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b');
132
+ });
133
+
134
+ test('keeps wallet sessions internal instead of projecting them through Features', async () => {
120
135
  const device = Device.fromDescriptor({ id: 'one', path: 'one' } as never);
121
136
  device.features = {
122
137
  protocol: 'V1',
@@ -133,11 +148,14 @@ describe('ClearSessionCache', () => {
133
148
  } as never;
134
149
 
135
150
  expect(device.state).not.toHaveProperty('session');
151
+ expect(device.getInternalState()).toBe('session-a');
136
152
  expect(device.features).toMatchObject({
137
- sessionId: 'session-a',
138
- session_id: 'session-a',
139
- passphraseState: 'hidden-a',
153
+ sessionId: null,
154
+ session_id: null,
140
155
  });
156
+ expect(device.features).not.toHaveProperty('passphraseState');
157
+ expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('sessionId');
158
+ expect(device.features?.raw?.protocolV1Features).not.toHaveProperty('session_id');
141
159
 
142
160
  const method = new ClearSessionCache({
143
161
  payload: {
@@ -151,10 +169,20 @@ describe('ClearSessionCache', () => {
151
169
 
152
170
  expect(device.features?.sessionId).toBeNull();
153
171
  expect(device.features?.session_id).toBeNull();
154
- expect(device.state?.raw?.protocolV1Features).toMatchObject({
172
+ expect(device.state?.raw?.protocolV1Features).not.toHaveProperty('sessionId');
173
+ expect(device.state?.raw?.protocolV1Features).not.toHaveProperty('session_id');
174
+ });
175
+
176
+ test('uses only the transport path as the temporary Protocol V2 cache key', () => {
177
+ const device = Device.fromDescriptor({ id: 'descriptor-id' } as never);
178
+ device.features = {
179
+ protocol: 'V2',
180
+ passphraseState: 'hidden-a',
155
181
  sessionId: 'session-a',
156
- session_id: 'session-a',
157
- });
182
+ } as never;
183
+
184
+ expect(device.getInternalState()).toBeUndefined();
185
+ expect(deviceWalletSessionStore.get('descriptor-id', 'hidden-a')).toBeUndefined();
158
186
  });
159
187
 
160
188
  test('clears one wallet without using a device', async () => {
@@ -211,3 +239,79 @@ describe('ClearSessionCache', () => {
211
239
  });
212
240
  });
213
241
  });
242
+
243
+ describe('Protocol V1 wallet identity initialization', () => {
244
+ beforeEach(() => {
245
+ deviceWalletSessionStore.clear();
246
+ jest.restoreAllMocks();
247
+ });
248
+
249
+ test('verifies the live device id before sending a cached wallet session', async () => {
250
+ const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
251
+ device.features = {
252
+ protocol: 'V1',
253
+ deviceId: 'cached-device-a',
254
+ unlocked: true,
255
+ passphraseProtection: true,
256
+ } as never;
257
+ deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
258
+ const typedCall = jest.fn().mockResolvedValue({
259
+ type: 'Features',
260
+ message: { device_id: 'live-device-b' },
261
+ });
262
+ device.commands = { typedCall } as never;
263
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
264
+
265
+ await expect(
266
+ device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
267
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
268
+ expect(typedCall).toHaveBeenCalledTimes(1);
269
+ expect(typedCall).toHaveBeenCalledWith(
270
+ 'Initialize',
271
+ 'Features',
272
+ expect.not.objectContaining({ session_id: 'session-a' }),
273
+ expect.any(Object)
274
+ );
275
+ });
276
+
277
+ test('resumes a cached V1 wallet only after the live device id matches', async () => {
278
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
279
+ device.features = {
280
+ protocol: 'V1',
281
+ deviceId: 'device-a',
282
+ unlocked: true,
283
+ passphraseProtection: true,
284
+ } as never;
285
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
286
+ const typedCall = jest
287
+ .fn()
288
+ .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
289
+ .mockResolvedValueOnce({
290
+ type: 'Features',
291
+ message: { device_id: 'device-a', session_id: 'session-a' },
292
+ });
293
+ device.commands = { typedCall } as never;
294
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
295
+
296
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
297
+
298
+ expect(typedCall).toHaveBeenCalledTimes(2);
299
+ expect(typedCall).toHaveBeenNthCalledWith(
300
+ 1,
301
+ 'Initialize',
302
+ 'Features',
303
+ expect.not.objectContaining({ session_id: 'session-a' }),
304
+ expect.any(Object)
305
+ );
306
+ expect(typedCall).toHaveBeenNthCalledWith(
307
+ 2,
308
+ 'Initialize',
309
+ 'Features',
310
+ expect.objectContaining({
311
+ session_id: 'session-a',
312
+ passphrase_state: 'hidden-a',
313
+ }),
314
+ expect.any(Object)
315
+ );
316
+ });
317
+ });
@@ -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
+ });
@@ -0,0 +1,70 @@
1
+ import {
2
+ getFirmwareUpdateHostBindingGeneration,
3
+ registerFirmwareUpdateHostBinding,
4
+ resolveFirmwareUpdateHostBinding,
5
+ unregisterFirmwareUpdateHostBinding,
6
+ } from '../../src/api/firmware/FirmwareHostBinding';
7
+
8
+ const createBinding = () => ({
9
+ artifactReader: {
10
+ open: jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: 4 })),
11
+ read: jest.fn(() =>
12
+ Promise.resolve({
13
+ data: new ArrayBuffer(4),
14
+ bytesRead: 4,
15
+ eof: true,
16
+ })
17
+ ),
18
+ close: jest.fn(() => Promise.resolve()),
19
+ },
20
+ });
21
+
22
+ describe('firmware host binding generation', () => {
23
+ afterEach(() => {
24
+ unregisterFirmwareUpdateHostBinding();
25
+ });
26
+
27
+ test('invalidates callbacks from a replaced generation', async () => {
28
+ const staleBinding = createBinding();
29
+ const staleGeneration = registerFirmwareUpdateHostBinding(staleBinding);
30
+ const stale = resolveFirmwareUpdateHostBinding(staleGeneration);
31
+ const activeGeneration = registerFirmwareUpdateHostBinding(createBinding());
32
+
33
+ expect(activeGeneration).toBeGreaterThan(staleGeneration);
34
+ expect(getFirmwareUpdateHostBindingGeneration()).toBe(activeGeneration);
35
+ await expect(
36
+ stale.artifactReader.read({
37
+ readerId: 'reader-1',
38
+ offset: 0,
39
+ length: 4,
40
+ })
41
+ ).rejects.toMatchObject({
42
+ params: {
43
+ firmwareUpdateCode: 'FirmwareArtifactReaderInvalid',
44
+ },
45
+ });
46
+ expect(unregisterFirmwareUpdateHostBinding(staleGeneration)).toBe(false);
47
+ expect(getFirmwareUpdateHostBindingGeneration()).toBe(activeGeneration);
48
+ });
49
+
50
+ test('closes an artifact opened during a generation race', async () => {
51
+ const binding = createBinding();
52
+ binding.artifactReader.open.mockImplementationOnce(() => {
53
+ registerFirmwareUpdateHostBinding(createBinding());
54
+ return Promise.resolve({ readerId: 'stale-reader', size: 4 });
55
+ });
56
+ const generation = registerFirmwareUpdateHostBinding(binding);
57
+ const guarded = resolveFirmwareUpdateHostBinding(generation);
58
+
59
+ await expect(
60
+ guarded.artifactReader.open({ artifactRef: `fw:${'a'.repeat(64)}` })
61
+ ).rejects.toMatchObject({
62
+ params: {
63
+ firmwareUpdateCode: 'FirmwareArtifactReaderInvalid',
64
+ },
65
+ });
66
+ expect(binding.artifactReader.close).toHaveBeenCalledWith({
67
+ readerId: 'stale-reader',
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,13 @@
1
+ import { getFirmwareUpdateCapabilities } from '../../src/api/firmware/FirmwareUpdateCapabilities';
2
+
3
+ describe('getFirmwareUpdateCapabilities', () => {
4
+ it('advertises only the prepared artifact contract', () => {
5
+ expect(getFirmwareUpdateCapabilities()).toEqual({
6
+ planSchemaVersion: 2,
7
+ preparedPlanSchemaVersion: 2,
8
+ hostBindingProtocolVersion: 2,
9
+ manifestModes: ['external-only', 'sdk-managed'],
10
+ supportsArtifactReader: true,
11
+ });
12
+ });
13
+ });