@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
@@ -0,0 +1,326 @@
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
+
3
+ import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+
5
+ import type { Features } from '../../src/types';
6
+
7
+ const createFeatures = ({
8
+ deviceType,
9
+ firmwareVersion = '1.0.0',
10
+ bootloaderVersion = '1.0.0',
11
+ }: {
12
+ deviceType: EDeviceType;
13
+ firmwareVersion?: string;
14
+ bootloaderVersion?: string;
15
+ }) =>
16
+ ({
17
+ deviceType,
18
+ serialNo: `${deviceType}-device-id`,
19
+ firmwareVersion,
20
+ bootloaderVersion,
21
+ } as Features);
22
+
23
+ const noUpdate = {
24
+ status: 'valid',
25
+ release: undefined,
26
+ };
27
+
28
+ describe('buildFirmwareUpdatePlan', () => {
29
+ test('selects all legacy artifacts before execution and chooses the desktop full resource', () => {
30
+ const plan = buildFirmwareUpdatePlan({
31
+ features: createFeatures({
32
+ deviceType: EDeviceType.Classic1s,
33
+ firmwareVersion: '1.0.0',
34
+ }),
35
+ firmwareType: EFirmwareType.Universal,
36
+ platform: 'desktop',
37
+ firmware: {
38
+ status: 'outdated',
39
+ release: {
40
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
41
+ expectedSize: 1024,
42
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
43
+ version: [3, 0, 0],
44
+ resource: 'https://firmware.onekey.so/classic/resource.zip',
45
+ fullResource: 'https://firmware.onekey.so/classic/full-resource.zip',
46
+ fullResourceExpectedSize: 4096,
47
+ fullResourceFingerprint:
48
+ '2222222222222222222222222222222222222222222222222222222222222222',
49
+ fullResourceRange: ['2.0.0', '3.0.0'],
50
+ },
51
+ },
52
+ ble: {
53
+ status: 'outdated',
54
+ release: {
55
+ webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
56
+ expectedSize: 512,
57
+ fingerprintWeb: '3333333333333333333333333333333333333333333333333333333333333333',
58
+ },
59
+ },
60
+ bootloader: {
61
+ status: 'outdated',
62
+ shouldUpdate: true,
63
+ release: {
64
+ bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
65
+ bootloaderExpectedSize: 768,
66
+ bootloaderFingerprint: '4444444444444444444444444444444444444444444444444444444444444444',
67
+ bootloaderVersion: [2, 0, 0],
68
+ },
69
+ },
70
+ });
71
+
72
+ expect(plan.executor).toBe('v2');
73
+ expect(plan.artifacts).toEqual([
74
+ expect.objectContaining({
75
+ artifactId: 'bootloader',
76
+ role: 'bootloader',
77
+ expectedSize: 768,
78
+ targetVersion: '2.0.0',
79
+ }),
80
+ expect.objectContaining({
81
+ artifactId: 'firmware',
82
+ role: 'firmware',
83
+ expectedSize: 1024,
84
+ targetVersion: '3.0.0',
85
+ }),
86
+ expect.objectContaining({
87
+ artifactId: 'resource',
88
+ role: 'resource',
89
+ url: 'https://firmware.onekey.so/classic/full-resource.zip',
90
+ expectedSize: 4096,
91
+ }),
92
+ expect.objectContaining({
93
+ artifactId: 'ble',
94
+ role: 'ble',
95
+ expectedSize: 512,
96
+ }),
97
+ ]);
98
+ expect(plan.targetsToUpdate).toEqual(['bootloader', 'firmware', 'resource', 'ble']);
99
+ });
100
+
101
+ test('selects V3 for Pro with bootloader 2.8 or newer', () => {
102
+ const plan = buildFirmwareUpdatePlan({
103
+ features: createFeatures({
104
+ deviceType: EDeviceType.Pro,
105
+ bootloaderVersion: '2.8.0',
106
+ }),
107
+ firmwareType: EFirmwareType.BitcoinOnly,
108
+ platform: 'native',
109
+ firmware: {
110
+ status: 'outdated',
111
+ release: {
112
+ url: 'https://firmware.onekey.so/pro/firmware.bin',
113
+ version: [4, 0, 0],
114
+ },
115
+ },
116
+ ble: noUpdate,
117
+ bootloader: noUpdate,
118
+ });
119
+
120
+ expect(plan.executor).toBe('v3');
121
+ expect(plan.firmwareType).toBe(EFirmwareType.BitcoinOnly);
122
+ expect(plan.platform).toBe('native');
123
+ });
124
+
125
+ test('rejects a prepared native plan without a stable device identity', () => {
126
+ const features = createFeatures({
127
+ deviceType: EDeviceType.Classic1s,
128
+ });
129
+ delete (features as Partial<Features>).serialNo;
130
+
131
+ expect(() =>
132
+ buildFirmwareUpdatePlan({
133
+ features,
134
+ firmwareType: EFirmwareType.Universal,
135
+ platform: 'native',
136
+ firmware: {
137
+ status: 'outdated',
138
+ release: {
139
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
140
+ },
141
+ },
142
+ ble: noUpdate,
143
+ bootloader: noUpdate,
144
+ })
145
+ ).toThrow('requires a stable device identity');
146
+ });
147
+
148
+ test('maps the Pro2 component order and resource bundles without downloading them', () => {
149
+ const plan = buildFirmwareUpdatePlan({
150
+ features: createFeatures({ deviceType: EDeviceType.Pro2 }),
151
+ firmwareType: EFirmwareType.Universal,
152
+ platform: 'desktop',
153
+ firmware: {
154
+ status: 'required',
155
+ release: {
156
+ installOrder: ['bootloader', 'applicationP1'],
157
+ components: {
158
+ applicationP1: {
159
+ target: 'APPLICATION_P1',
160
+ url: 'https://firmware.onekey.so/pro2/application-p1.bin',
161
+ expectedSize: 1024,
162
+ fingerprint: '1111111111111111111111111111111111111111111111111111111111111111',
163
+ },
164
+ bootloader: {
165
+ target: 'BOOTLOADER',
166
+ url: 'https://firmware.onekey.so/pro2/bootloader.bin',
167
+ },
168
+ se01: {
169
+ target: 'SE01',
170
+ url: 'https://firmware.onekey.so/pro2/se01.bin',
171
+ },
172
+ },
173
+ resourceBundles: [
174
+ {
175
+ name: 'images',
176
+ url: 'https://firmware.onekey.so/pro2/images.okpkg',
177
+ },
178
+ ],
179
+ },
180
+ },
181
+ ble: noUpdate,
182
+ bootloader: noUpdate,
183
+ });
184
+
185
+ expect(plan.executor).toBe('v4');
186
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual([
187
+ 'component:boot',
188
+ 'component:app_v1',
189
+ 'component:se01',
190
+ 'resourceBundle:images',
191
+ ]);
192
+ expect(plan.targetsToUpdate).toEqual(['boot', 'app_v1', 'se01', 'resource']);
193
+ expect(plan.artifacts[1]).toEqual(
194
+ expect.objectContaining({
195
+ expectedSize: 1024,
196
+ expectedSha256: '1111111111111111111111111111111111111111111111111111111111111111',
197
+ })
198
+ );
199
+ });
200
+
201
+ test('rejects Pro2 ROMLOADER because it requires a separate loader flow', () => {
202
+ expect(() =>
203
+ buildFirmwareUpdatePlan({
204
+ features: createFeatures({ deviceType: EDeviceType.Pro2 }),
205
+ firmwareType: EFirmwareType.Universal,
206
+ platform: 'desktop',
207
+ firmware: {
208
+ status: 'required',
209
+ release: {
210
+ components: {
211
+ romloader: {
212
+ target: 'ROMLOADER',
213
+ url: 'https://firmware.onekey.so/pro2/romloader.bin',
214
+ },
215
+ },
216
+ },
217
+ },
218
+ ble: noUpdate,
219
+ bootloader: noUpdate,
220
+ })
221
+ ).toThrow('ROMLOADER requires its dedicated loader flow');
222
+ });
223
+
224
+ test.each([
225
+ {
226
+ label: 'component target',
227
+ release: {
228
+ components: {
229
+ application: {
230
+ target: 'APPLICATION_P1',
231
+ url: 'https://firmware.onekey.so/pro2/application.bin',
232
+ },
233
+ applicationDuplicate: {
234
+ target: 'APPLICATION_P1',
235
+ url: 'https://firmware.onekey.so/pro2/application-duplicate.bin',
236
+ },
237
+ },
238
+ },
239
+ expectedError: 'duplicates target app_v1',
240
+ },
241
+ {
242
+ label: 'resource bundle name',
243
+ release: {
244
+ components: {
245
+ application: {
246
+ target: 'APPLICATION_P1',
247
+ url: 'https://firmware.onekey.so/pro2/application.bin',
248
+ },
249
+ },
250
+ resourceBundles: [
251
+ {
252
+ name: 'images',
253
+ url: 'https://firmware.onekey.so/pro2/images.okpkg',
254
+ },
255
+ {
256
+ name: 'images',
257
+ url: 'https://firmware.onekey.so/pro2/images-duplicate.okpkg',
258
+ },
259
+ ],
260
+ },
261
+ expectedError: 'duplicates name images',
262
+ },
263
+ ])('rejects a duplicate Pro2 $label', ({ release, expectedError }) => {
264
+ expect(() =>
265
+ buildFirmwareUpdatePlan({
266
+ features: createFeatures({ deviceType: EDeviceType.Pro2 }),
267
+ firmwareType: EFirmwareType.Universal,
268
+ platform: 'desktop',
269
+ firmware: {
270
+ status: 'required',
271
+ release,
272
+ },
273
+ ble: noUpdate,
274
+ bootloader: noUpdate,
275
+ })
276
+ ).toThrow(expectedError);
277
+ });
278
+
279
+ test('produces a deterministic digest and binds it to the platform', () => {
280
+ const input = {
281
+ features: createFeatures({ deviceType: EDeviceType.Classic1s }),
282
+ firmwareType: EFirmwareType.Universal,
283
+ firmware: noUpdate,
284
+ ble: noUpdate,
285
+ bootloader: noUpdate,
286
+ };
287
+ const first = buildFirmwareUpdatePlan({ ...input, platform: 'native' });
288
+ const second = buildFirmwareUpdatePlan({ ...input, platform: 'native' });
289
+ const desktop = buildFirmwareUpdatePlan({ ...input, platform: 'desktop' });
290
+
291
+ expect(first.planDigest).toMatch(/^[a-f0-9]{64}$/);
292
+ expect(first.planDigest).toBe(second.planDigest);
293
+ expect(first.planDigest).not.toBe(desktop.planDigest);
294
+ });
295
+
296
+ test('binds trusted artifact integrity metadata into the plan digest', () => {
297
+ const createPlan = (fingerprint: string) =>
298
+ buildFirmwareUpdatePlan({
299
+ features: createFeatures({ deviceType: EDeviceType.Classic1s }),
300
+ firmwareType: EFirmwareType.Universal,
301
+ platform: 'native',
302
+ firmware: {
303
+ status: 'outdated',
304
+ release: {
305
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
306
+ version: [3, 0, 0],
307
+ expectedSize: 1024,
308
+ fingerprint,
309
+ },
310
+ },
311
+ ble: noUpdate,
312
+ bootloader: noUpdate,
313
+ });
314
+
315
+ const first = createPlan('1111111111111111111111111111111111111111111111111111111111111111');
316
+ const second = createPlan('2222222222222222222222222222222222222222222222222222222222222222');
317
+
318
+ expect(first.artifacts[0]).toEqual(
319
+ expect.objectContaining({
320
+ expectedSize: 1024,
321
+ expectedSha256: '1111111111111111111111111111111111111111111111111111111111111111',
322
+ })
323
+ );
324
+ expect(first.planDigest).not.toBe(second.planDigest);
325
+ });
326
+ });
@@ -0,0 +1,200 @@
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
+
3
+ import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+ import {
5
+ assertFirmwareUpdatePreparedPlanBinding,
6
+ prepareFirmwareUpdatePlan,
7
+ validateFirmwareUpdatePreparedPlan,
8
+ } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
9
+
10
+ import type { Features } from '../../src/types';
11
+
12
+ const buildPlan = () =>
13
+ buildFirmwareUpdatePlan({
14
+ features: {
15
+ deviceType: EDeviceType.Classic1s,
16
+ serialNo: 'classic1s-device-id',
17
+ firmwareVersion: '1.0.0',
18
+ bootloaderVersion: '1.0.0',
19
+ } as Features,
20
+ firmwareType: EFirmwareType.Universal,
21
+ platform: 'native',
22
+ firmware: {
23
+ status: 'outdated',
24
+ release: {
25
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
26
+ expectedSize: 4,
27
+ fingerprint: 'a'.repeat(64),
28
+ version: [3, 0, 0],
29
+ resource: 'https://firmware.onekey.so/classic/resource.zip',
30
+ resourceExpectedSize: 8,
31
+ resourceFingerprint: 'b'.repeat(64),
32
+ },
33
+ },
34
+ ble: {
35
+ status: 'valid',
36
+ release: undefined,
37
+ },
38
+ bootloader: {
39
+ status: 'valid',
40
+ release: undefined,
41
+ },
42
+ });
43
+
44
+ const artifact = (sha256: string, size: number) => ({
45
+ artifactRef: `fw:${sha256}`,
46
+ size,
47
+ sha256,
48
+ });
49
+
50
+ describe('FirmwareUpdatePreparedPlan', () => {
51
+ test('binds all receipts into a URL-free, network-forbidden plan', () => {
52
+ const plan = buildPlan();
53
+ const prepared = prepareFirmwareUpdatePlan({
54
+ plan,
55
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
56
+ artifacts: [
57
+ {
58
+ artifactId: 'firmware',
59
+ artifact: artifact('a'.repeat(64), 4),
60
+ },
61
+ {
62
+ artifactId: 'resource',
63
+ artifact: artifact('b'.repeat(64), 8),
64
+ materializedEntries: [
65
+ {
66
+ entryName: 'resource/index.bin',
67
+ artifact: artifact('c'.repeat(64), 2),
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ });
73
+
74
+ expect(prepared.networkPolicy).toBe('forbid');
75
+ expect(prepared.planDigest).toBe(plan.planDigest);
76
+ expect(prepared.preparedPlanDigest).toMatch(/^[a-f0-9]{64}$/u);
77
+ expect(JSON.stringify(prepared)).not.toContain('https://');
78
+ expect(validateFirmwareUpdatePreparedPlan(prepared)).toEqual(prepared);
79
+ expect(
80
+ assertFirmwareUpdatePreparedPlanBinding({
81
+ preparedPlan: prepared,
82
+ executor: 'v2',
83
+ platform: 'native',
84
+ scopeTargets: ['firmware', 'resource'],
85
+ bindings: [
86
+ {
87
+ target: 'firmware',
88
+ artifact: artifact('a'.repeat(64), 4),
89
+ },
90
+ {
91
+ target: 'resource',
92
+ entryName: 'resource/index.bin',
93
+ artifact: artifact('c'.repeat(64), 2),
94
+ },
95
+ ],
96
+ })
97
+ ).toEqual(prepared);
98
+ expect(() =>
99
+ assertFirmwareUpdatePreparedPlanBinding({
100
+ preparedPlan: prepared,
101
+ executor: 'v2',
102
+ platform: 'native',
103
+ scopeTargets: ['firmware'],
104
+ bindings: [
105
+ {
106
+ target: 'firmware',
107
+ artifact: artifact('b'.repeat(64), 8),
108
+ },
109
+ ],
110
+ })
111
+ ).toThrow('artifact binding is invalid');
112
+ });
113
+
114
+ test('rejects receipt drift and a tampered update plan digest', () => {
115
+ const plan = buildPlan();
116
+ expect(() =>
117
+ prepareFirmwareUpdatePlan({
118
+ plan,
119
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
120
+ artifacts: [
121
+ {
122
+ artifactId: 'firmware',
123
+ artifact: artifact('d'.repeat(64), 4),
124
+ },
125
+ {
126
+ artifactId: 'resource',
127
+ artifact: artifact('b'.repeat(64), 8),
128
+ materializedEntries: [
129
+ {
130
+ entryName: 'resource/index.bin',
131
+ artifact: artifact('c'.repeat(64), 2),
132
+ },
133
+ ],
134
+ },
135
+ ],
136
+ })
137
+ ).toThrow('receipt does not match');
138
+
139
+ expect(() =>
140
+ prepareFirmwareUpdatePlan({
141
+ plan: { ...plan, planDigest: 'f'.repeat(64) },
142
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
143
+ artifacts: [],
144
+ })
145
+ ).toThrow('plan digest is invalid');
146
+ });
147
+
148
+ test('projects portable archive paths to unique device resource names', () => {
149
+ const plan = buildPlan();
150
+ const buildArtifacts = (entryNames: string[]) => [
151
+ {
152
+ artifactId: 'firmware',
153
+ artifact: artifact('a'.repeat(64), 4),
154
+ },
155
+ {
156
+ artifactId: 'resource',
157
+ artifact: artifact('b'.repeat(64), 8),
158
+ materializedEntries: entryNames.map((entryName, index) => ({
159
+ entryName,
160
+ artifact: artifact(String(index + 1).repeat(64), 2),
161
+ })),
162
+ },
163
+ ];
164
+
165
+ expect(() =>
166
+ prepareFirmwareUpdatePlan({
167
+ plan,
168
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
169
+ artifacts: buildArtifacts(['icons/index.bin', 'fonts/index.bin']),
170
+ })
171
+ ).toThrow('duplicate entry names');
172
+ expect(() =>
173
+ prepareFirmwareUpdatePlan({
174
+ plan,
175
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
176
+ artifacts: buildArtifacts(['../index.bin']),
177
+ })
178
+ ).toThrow('entry name is invalid');
179
+ });
180
+
181
+ test('rejects unknown fields even when a caller presents a plan-shaped object', () => {
182
+ const plan = buildPlan();
183
+ expect(() =>
184
+ prepareFirmwareUpdatePlan({
185
+ plan: {
186
+ ...plan,
187
+ artifacts: [
188
+ {
189
+ ...plan.artifacts[0],
190
+ unexpectedUrl: 'https://attacker.invalid/payload',
191
+ },
192
+ plan.artifacts[1],
193
+ ],
194
+ } as typeof plan,
195
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
196
+ artifacts: [],
197
+ })
198
+ ).toThrow('invalid field set');
199
+ });
200
+ });
@@ -2,7 +2,7 @@ import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shar
2
2
 
3
3
  import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
4
4
  import { getBinary } from '../../src/api/firmware/getBinary';
5
- import { uploadFirmware } from '../../src/api/firmware/uploadFirmware';
5
+ import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
6
6
  import * as utils from '../../src/utils';
7
7
 
8
8
  jest.mock('../../src/data/config', () => ({
@@ -18,7 +18,8 @@ jest.mock('../../src/api/firmware/getBinary', () => ({
18
18
 
19
19
  jest.mock('../../src/api/firmware/uploadFirmware', () => ({
20
20
  updateResources: jest.fn(),
21
- uploadFirmware: jest.fn(),
21
+ updateResourcesFromSources: jest.fn(),
22
+ uploadFirmwareFromSource: jest.fn(),
22
23
  }));
23
24
 
24
25
  jest.mock('../../src/device/DevicePool', () => ({
@@ -29,7 +30,9 @@ jest.mock('../../src/device/DevicePool', () => ({
29
30
  }));
30
31
 
31
32
  const mockGetBinary = getBinary as jest.MockedFunction<typeof getBinary>;
32
- const mockUploadFirmware = uploadFirmware as jest.MockedFunction<typeof uploadFirmware>;
33
+ const mockUploadFirmwareFromSource = uploadFirmwareFromSource as jest.MockedFunction<
34
+ typeof uploadFirmwareFromSource
35
+ >;
33
36
 
34
37
  type DeviceType = 'CLASSIC1S' | 'PURE';
35
38
 
@@ -175,7 +178,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
175
178
  expect(mockGetBinary).toHaveBeenCalledTimes(1);
176
179
  expectNoFirmwareMutationCalls(typedCall);
177
180
  expect(acquire).not.toHaveBeenCalled();
178
- expect(mockUploadFirmware).not.toHaveBeenCalled();
181
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
179
182
  });
180
183
 
181
184
  it.each([
@@ -216,9 +219,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
216
219
  binary: firmwareBinary,
217
220
  } as Awaited<ReturnType<typeof getBinary>>);
218
221
  });
219
- mockUploadFirmware.mockImplementation(() => {
222
+ mockUploadFirmwareFromSource.mockImplementation(() => {
220
223
  callOrder.push('upload');
221
- return Promise.resolve(expectedResponse as Awaited<ReturnType<typeof uploadFirmware>>);
224
+ return Promise.resolve(
225
+ expectedResponse as Awaited<ReturnType<typeof uploadFirmwareFromSource>>
226
+ );
222
227
  });
223
228
  const { method, typedCall } = createMethod({
224
229
  updateType,
@@ -248,15 +253,15 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
248
253
  },
249
254
  })
250
255
  );
251
- expect(mockUploadFirmware).toHaveBeenCalledWith(
256
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
252
257
  updateType,
253
258
  expect.any(Function),
254
259
  expect.any(Function),
255
260
  method.device,
256
- {
257
- payload: firmwareBinary,
258
- rebootOnSuccess: true,
259
- },
261
+ expect.objectContaining({
262
+ size: firmwareBinary.byteLength,
263
+ }),
264
+ true,
260
265
  isUpdateBootloader
261
266
  );
262
267
  }
@@ -271,11 +276,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
271
276
  binary: firmwareBinary,
272
277
  } as Awaited<ReturnType<typeof getBinary>>);
273
278
  });
274
- mockUploadFirmware.mockImplementation(() => {
279
+ mockUploadFirmwareFromSource.mockImplementation(() => {
275
280
  callOrder.push('upload');
276
281
  return Promise.resolve({
277
282
  success: true,
278
- } as Awaited<ReturnType<typeof uploadFirmware>>);
283
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
279
284
  });
280
285
  const { method, typedCall, acquire } = createMethod({
281
286
  bootloaderMode: true,
@@ -300,9 +305,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
300
305
  resolveDownload = resolve;
301
306
  })
302
307
  );
303
- mockUploadFirmware.mockResolvedValue({
308
+ mockUploadFirmwareFromSource.mockResolvedValue({
304
309
  success: true,
305
- } as Awaited<ReturnType<typeof uploadFirmware>>);
310
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
306
311
  const { method, typedCall } = createMethod();
307
312
 
308
313
  const runPromise = method.run();
@@ -333,7 +338,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
333
338
 
334
339
  expectNoFirmwareMutationCalls(typedCall);
335
340
  expect(acquire).not.toHaveBeenCalled();
336
- expect(mockUploadFirmware).not.toHaveBeenCalled();
341
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
337
342
  });
338
343
 
339
344
  it.each([
@@ -342,23 +347,23 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
342
347
  ])(
343
348
  'keeps the non-empty %s binary overload and never downloads it again',
344
349
  async (_kind, binary) => {
345
- mockUploadFirmware.mockResolvedValue({
350
+ mockUploadFirmwareFromSource.mockResolvedValue({
346
351
  success: true,
347
- } as Awaited<ReturnType<typeof uploadFirmware>>);
352
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
348
353
  const { method } = createMethod({ binary });
349
354
 
350
355
  await method.run();
351
356
 
352
357
  expect(mockGetBinary).not.toHaveBeenCalled();
353
- expect(mockUploadFirmware).toHaveBeenCalledWith(
358
+ expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
354
359
  'firmware',
355
360
  expect.any(Function),
356
361
  expect.any(Function),
357
362
  method.device,
358
- {
359
- payload: binary,
360
- rebootOnSuccess: true,
361
- },
363
+ expect.objectContaining({
364
+ size: binary.byteLength,
365
+ }),
366
+ true,
362
367
  undefined
363
368
  );
364
369
  }
@@ -369,17 +374,21 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
369
374
  ['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
370
375
  ['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
371
376
  ])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
372
- mockUploadFirmware.mockResolvedValue({
373
- success: true,
374
- } as Awaited<ReturnType<typeof uploadFirmware>>);
377
+ let uploadedBytes: number[] = [];
378
+ mockUploadFirmwareFromSource.mockImplementation(
379
+ async (_updateType, _typedCall, _postMessage, _device, source) => {
380
+ uploadedBytes = Array.from(new Uint8Array(await source.readAt(0, source.size)));
381
+ return {
382
+ success: true,
383
+ } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>;
384
+ }
385
+ );
375
386
  const { method } = createMethod({ binary });
376
387
 
377
388
  await method.run();
378
389
 
379
390
  expect(mockGetBinary).not.toHaveBeenCalled();
380
- const normalizedBinary = mockUploadFirmware.mock.calls[0][4].payload;
381
- expect(normalizedBinary).toBeInstanceOf(ArrayBuffer);
382
- expect(Array.from(new Uint8Array(normalizedBinary))).toEqual(expectedBytes);
391
+ expect(uploadedBytes).toEqual(expectedBytes);
383
392
  });
384
393
 
385
394
  it.each([
@@ -397,6 +406,6 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
397
406
  expect(mockGetBinary).not.toHaveBeenCalled();
398
407
  expectNoFirmwareMutationCalls(typedCall);
399
408
  expect(acquire).not.toHaveBeenCalled();
400
- expect(mockUploadFirmware).not.toHaveBeenCalled();
409
+ expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
401
410
  });
402
411
  });