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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +203 -27
  3. package/__tests__/base64Data.test.ts +69 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +89 -38
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +83 -14
  6. package/__tests__/device-connector-protocol.test.ts +21 -0
  7. package/__tests__/device-lifecycle-events.test.ts +427 -7
  8. package/__tests__/device-pool-state.test.ts +25 -0
  9. package/__tests__/device-settings.test.ts +237 -9
  10. package/__tests__/device-state-events.test.ts +2 -2
  11. package/__tests__/device-state-mapper.test.ts +61 -5
  12. package/__tests__/device-utils.test.ts +15 -1
  13. package/__tests__/device-wallet-session-store.test.ts +147 -21
  14. package/__tests__/deviceSettings.test.ts +7 -3
  15. package/__tests__/deviceUploadNft.test.ts +36 -4
  16. package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -3
  17. package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
  18. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
  19. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
  20. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +128 -2
  21. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  22. package/__tests__/firmware-update/firmware-update-plan.test.ts +114 -50
  23. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  24. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  25. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +678 -0
  26. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  27. package/__tests__/get-device-state.test.ts +60 -21
  28. package/__tests__/logBlockEvent.test.ts +13 -1
  29. package/__tests__/open-wallet-session.test.ts +127 -1
  30. package/__tests__/protocol-v2-firmware-targets.test.ts +39 -0
  31. package/__tests__/protocol-v2-resources.test.ts +185 -88
  32. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  33. package/__tests__/protocol-v2.test.ts +2274 -466
  34. package/__tests__/refresh-device-state.test.ts +3 -1
  35. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  36. package/__tests__/search-devices.test.ts +79 -32
  37. package/__tests__/ton-sign-message.test.ts +84 -0
  38. package/dist/api/BaseMethod.d.ts +2 -1
  39. package/dist/api/BaseMethod.d.ts.map +1 -1
  40. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV4.d.ts +22 -7
  45. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  46. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  47. package/dist/api/SearchDevices.d.ts.map +1 -1
  48. package/dist/api/UploadPortfolio.d.ts +1 -1
  49. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  50. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  51. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  54. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  55. package/dist/api/device/DeviceVerify.d.ts +1 -0
  56. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  57. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  59. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  61. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  63. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  65. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  66. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
  67. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  68. package/dist/api/firmware/getBinary.d.ts +3 -2
  69. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  70. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  71. package/dist/api/helpers/base64Data.d.ts +16 -0
  72. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  73. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  74. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  76. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  77. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  78. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  79. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  80. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  81. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  82. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  83. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  84. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  85. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  86. package/dist/core/index.d.ts +6 -1
  87. package/dist/core/index.d.ts.map +1 -1
  88. package/dist/data-manager/DataManager.d.ts +4 -3
  89. package/dist/data-manager/DataManager.d.ts.map +1 -1
  90. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  91. package/dist/device/Device.d.ts +16 -3
  92. package/dist/device/Device.d.ts.map +1 -1
  93. package/dist/device/DeviceCommands.d.ts +4 -4
  94. package/dist/device/DeviceCommands.d.ts.map +1 -1
  95. package/dist/device/DeviceConnector.d.ts +1 -1
  96. package/dist/device/DeviceConnector.d.ts.map +1 -1
  97. package/dist/device/DevicePool.d.ts.map +1 -1
  98. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  99. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  100. package/dist/events/logBlockEvent.d.ts.map +1 -1
  101. package/dist/events/ui-request.d.ts +3 -0
  102. package/dist/events/ui-request.d.ts.map +1 -1
  103. package/dist/index.d.ts +54 -84
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2333 -1515
  106. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  107. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  108. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  109. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  110. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  111. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  113. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  114. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  115. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  116. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  117. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  118. package/dist/types/api/protocolV2.d.ts +1 -1
  119. package/dist/types/api/protocolV2.d.ts.map +1 -1
  120. package/dist/types/device.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +3 -31
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  124. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  125. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  126. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  127. package/dist/utils/deviceSettings.d.ts +1 -0
  128. package/dist/utils/deviceSettings.d.ts.map +1 -1
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/patch.d.ts +1 -1
  132. package/dist/utils/patch.d.ts.map +1 -1
  133. package/dist/utils/pro2Nft.d.ts +7 -0
  134. package/dist/utils/pro2Nft.d.ts.map +1 -1
  135. package/package.json +6 -4
  136. package/src/api/BaseMethod.ts +8 -1
  137. package/src/api/CheckAllFirmwareRelease.ts +8 -5
  138. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  139. package/src/api/CheckBootloaderRelease.ts +3 -2
  140. package/src/api/CheckFirmwareRelease.ts +2 -2
  141. package/src/api/FirmwareUpdateV2.ts +130 -69
  142. package/src/api/FirmwareUpdateV3.ts +29 -19
  143. package/src/api/FirmwareUpdateV4.ts +966 -743
  144. package/src/api/OpenWalletSession.ts +39 -9
  145. package/src/api/PromptWebDeviceAccess.ts +2 -2
  146. package/src/api/SearchDevices.ts +6 -3
  147. package/src/api/UploadPortfolio.ts +9 -2
  148. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  149. package/src/api/device/DeviceChangePin.ts +4 -1
  150. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  151. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  152. package/src/api/device/DeviceSettings.ts +34 -20
  153. package/src/api/device/DeviceVerify.ts +25 -0
  154. package/src/api/device/DeviceWipe.ts +4 -1
  155. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  156. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  157. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  158. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +131 -76
  159. package/src/api/firmware/FirmwareUpdatePlan.ts +55 -82
  160. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +32 -15
  161. package/src/api/firmware/protocolV2Release.ts +7 -2
  162. package/src/api/helpers/base64Data.ts +85 -0
  163. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  164. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  165. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  166. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  167. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  168. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  169. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  170. package/src/api/protocol-v2/helpers.ts +8 -0
  171. package/src/api/ton/TonSignMessage.ts +5 -3
  172. package/src/core/index.ts +264 -55
  173. package/src/data/messages/messages-protocol-v2.json +140 -13
  174. package/src/data-manager/DataManager.ts +46 -39
  175. package/src/data-manager/TransportManager.ts +3 -1
  176. package/src/device/Device.ts +210 -44
  177. package/src/device/DeviceCommands.ts +27 -18
  178. package/src/device/DeviceConnector.ts +17 -3
  179. package/src/device/DevicePool.ts +9 -2
  180. package/src/device/DeviceStateMapper.ts +30 -24
  181. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  182. package/src/events/logBlockEvent.ts +14 -1
  183. package/src/events/ui-request.ts +3 -0
  184. package/src/index.ts +0 -10
  185. package/src/protocols/protocol-v2/features.ts +22 -5
  186. package/src/protocols/protocol-v2/index.ts +2 -0
  187. package/src/protocols/protocol-v2/resources.ts +110 -147
  188. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  189. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  190. package/src/types/api/firmwareUpdate.ts +1 -1
  191. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  192. package/src/types/api/protocolV2.ts +1 -1
  193. package/src/types/device.ts +2 -1
  194. package/src/types/settings.ts +6 -34
  195. package/src/utils/deviceFeaturesCompat.ts +9 -4
  196. package/src/utils/deviceFeaturesUtils.ts +4 -0
  197. package/src/utils/deviceInfoUtils.ts +9 -2
  198. package/src/utils/deviceSettings.ts +11 -4
  199. package/src/utils/index.ts +1 -0
  200. package/src/utils/pro2Nft.ts +31 -8
  201. package/__tests__/firmware-memory-host.test.ts +0 -112
  202. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  203. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  204. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
@@ -41,9 +41,15 @@ const FIRMWARE_UPDATE_CONFIRM = 'Firmware install confirmed';
41
41
  */
42
42
  export const BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS = 5000;
43
43
 
44
- type FirmwareTransferMetrics = Pick<
44
+ type FirmwareProgressMetadata = Pick<
45
45
  FirmwareProgress['payload'],
46
- 'transferredBytes' | 'totalBytes' | 'rateBytesPerSecond' | 'elapsedMs'
46
+ | 'transferredBytes'
47
+ | 'totalBytes'
48
+ | 'rateBytesPerSecond'
49
+ | 'elapsedMs'
50
+ | 'installTargetId'
51
+ | 'installPhase'
52
+ | 'installPhaseProgress'
47
53
  >;
48
54
 
49
55
  const isDeviceDisconnectedError = (error: unknown) => {
@@ -103,7 +109,7 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
103
109
  postProgressMessage = (
104
110
  progress: number,
105
111
  progressType: IFirmwareUpdateProgressType,
106
- metrics?: FirmwareTransferMetrics
112
+ metrics?: FirmwareProgressMetadata
107
113
  ) => {
108
114
  this.postMessage(
109
115
  createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, {
@@ -167,92 +173,141 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
167
173
  let checkCount = 0;
168
174
  let hasPromptedWebDevice = false;
169
175
  let isPromptingWebDevice = false;
170
- // eslint-disable-next-line prefer-const
176
+ let isFinished = false;
177
+ let intervalTimer: ReturnType<typeof setInterval> | undefined;
171
178
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
172
179
 
173
180
  const isTouchOrProDevice =
174
181
  this?.device?.getCurrentDeviceType() === EDeviceType.Touch ||
175
182
  this?.device?.getCurrentDeviceType() === EDeviceType.Pro;
176
183
 
177
- const intervalTimer: ReturnType<typeof setInterval> | undefined = setInterval(
178
- async () => {
179
- checkCount += 1;
180
- Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
181
- if (isTouchOrProDevice && isFirstCheck) {
182
- isFirstCheck = false;
183
- Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] wait 3000ms');
184
- await wait(3000);
185
- }
184
+ const clearPollingTimers = () => {
185
+ if (intervalTimer !== undefined) {
186
+ clearInterval(intervalTimer);
187
+ intervalTimer = undefined;
188
+ }
189
+ if (timeoutTimer !== undefined) {
190
+ clearTimeout(timeoutTimer);
191
+ timeoutTimer = undefined;
192
+ }
193
+ };
186
194
 
187
- if (
188
- checkCount > 4 &&
189
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
190
- !this.payload.skipWebDevicePrompt &&
191
- !hasPromptedWebDevice &&
192
- !isPromptingWebDevice
193
- ) {
194
- clearInterval(intervalTimer);
195
- clearTimeout(timeoutTimer);
196
- isPromptingWebDevice = true;
197
- try {
198
- this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
199
- const confirmed = await this._promptDeviceInBootloaderForWebDevice();
200
- hasPromptedWebDevice = true;
201
- if (confirmed) {
202
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
203
- }
204
- } catch (e) {
205
- clearInterval(intervalTimer);
206
- clearTimeout(timeoutTimer);
207
- Log.log(
208
- 'FirmwareUpdateBaseMethod [checkDeviceToBootloader] _promptDeviceInBootloaderForWebDevice failed: ',
209
- e
210
- );
211
- this.checkPromise?.reject(e);
212
- } finally {
213
- isPromptingWebDevice = false;
195
+ const checkForBootloader = async (clearActiveTimers: boolean) => {
196
+ const found = await this._checkDeviceInBootloaderMode(
197
+ connectId,
198
+ clearActiveTimers ? intervalTimer : undefined,
199
+ clearActiveTimers ? timeoutTimer : undefined
200
+ );
201
+ if (found) {
202
+ isFinished = true;
203
+ clearPollingTimers();
204
+ }
205
+ return found;
206
+ };
207
+
208
+ let startPolling: () => void = () => undefined;
209
+ const pollForBootloader = async () => {
210
+ if (isFinished || isPromptingWebDevice) return;
211
+ checkCount += 1;
212
+ Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
213
+ if (isTouchOrProDevice && isFirstCheck) {
214
+ isFirstCheck = false;
215
+ Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] wait 3000ms');
216
+ await wait(3000);
217
+ }
218
+
219
+ if (
220
+ checkCount > 4 &&
221
+ DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
222
+ !this.payload.skipWebDevicePrompt &&
223
+ !hasPromptedWebDevice &&
224
+ !isPromptingWebDevice
225
+ ) {
226
+ clearPollingTimers();
227
+ isPromptingWebDevice = true;
228
+ try {
229
+ this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
230
+ const confirmed = await this._promptDeviceInBootloaderForWebDevice();
231
+ hasPromptedWebDevice = true;
232
+ if (confirmed) {
233
+ await checkForBootloader(false);
234
+ }
235
+ // WebUSB enumeration can still be empty immediately after the chooser
236
+ // resolves. Resume a fresh bounded polling window instead of leaving the
237
+ // deferred check pending forever after the original timers were paused.
238
+ if (!isFinished) {
239
+ startPolling();
214
240
  }
215
- return;
241
+ } catch (e) {
242
+ isFinished = true;
243
+ clearPollingTimers();
244
+ Log.log(
245
+ 'FirmwareUpdateBaseMethod [checkDeviceToBootloader] _promptDeviceInBootloaderForWebDevice failed: ',
246
+ e
247
+ );
248
+ this.checkPromise?.reject(e);
249
+ } finally {
250
+ isPromptingWebDevice = false;
216
251
  }
252
+ return;
253
+ }
217
254
 
218
- if (isBleReconnect) {
219
- if (bleProbeInFlight) return;
220
- bleProbeInFlight = true;
221
- try {
222
- await this.device.deviceConnector?.acquire(
223
- this.device.originalDescriptor.id,
224
- null,
225
- true,
226
- this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
227
- );
228
- // Bound each probe so a request the rebooting device never received
229
- // frees the slot for the next tick instead of hanging into the
230
- // 30s reboot budget.
231
- await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
232
- if (this.device.isBootloader()) {
233
- clearInterval(intervalTimer);
234
- this.checkPromise?.resolve(true);
235
- }
236
- } catch (e) {
237
- // ignore error because of device is not connected
238
- Log.log('catch Bluetooth error when device is restarting: ', e);
239
- } finally {
240
- bleProbeInFlight = false;
255
+ if (isBleReconnect) {
256
+ if (bleProbeInFlight) return;
257
+ bleProbeInFlight = true;
258
+ try {
259
+ await this.device.deviceConnector?.acquire(
260
+ this.device.originalDescriptor.id,
261
+ null,
262
+ true,
263
+ this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
264
+ );
265
+ // Bound each probe so a request the rebooting device never received
266
+ // frees the slot for the next tick instead of hanging into the
267
+ // 30s reboot budget.
268
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
269
+ if (this.device.isBootloader()) {
270
+ isFinished = true;
271
+ clearPollingTimers();
272
+ this.checkPromise?.resolve(true);
241
273
  }
242
- } else {
243
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
274
+ } catch (e) {
275
+ // ignore error because of device is not connected
276
+ Log.log('catch Bluetooth error when device is restarting: ', e);
277
+ } finally {
278
+ bleProbeInFlight = false;
244
279
  }
245
- },
246
- isBleReconnect ? 3000 : 2000
247
- );
248
-
249
- // check goto bootloader mode timeout and throw error
250
- timeoutTimer = setTimeout(() => {
251
- if (this.checkPromise) {
252
- clearInterval(intervalTimer);
253
- this.checkPromise.reject(new Error());
280
+ } else {
281
+ await checkForBootloader(true);
254
282
  }
255
- }, 30000);
283
+ };
284
+
285
+ startPolling = () => {
286
+ if (isFinished) return;
287
+ clearPollingTimers();
288
+ intervalTimer = setInterval(
289
+ () => {
290
+ pollForBootloader().catch(error => {
291
+ if (isFinished) return;
292
+ isFinished = true;
293
+ clearPollingTimers();
294
+ this.checkPromise?.reject(error);
295
+ });
296
+ },
297
+ isBleReconnect ? 3000 : 2000
298
+ );
299
+ // Each automatic polling phase is bounded. Time spent in the browser's
300
+ // permission chooser is intentionally excluded from this deadline.
301
+ timeoutTimer = setTimeout(() => {
302
+ if (!isFinished && this.checkPromise) {
303
+ isFinished = true;
304
+ clearPollingTimers();
305
+ this.checkPromise.reject(new Error());
306
+ }
307
+ }, 30000);
308
+ };
309
+
310
+ startPolling();
256
311
  }
257
312
 
258
313
  private async _checkDeviceInBootloaderMode(
@@ -216,15 +216,20 @@ export const validateProtocolV2FirmwareUpdateTargets = (
216
216
  if (value === undefined) {
217
217
  return [];
218
218
  }
219
+ if (!Array.isArray(value)) {
220
+ return planError('Protocol V2 firmware update targets are invalid');
221
+ }
222
+ const normalizedTargets = value.map(target =>
223
+ target === 'boot_resources' ? 'resource' : target
224
+ );
219
225
  if (
220
- !Array.isArray(value) ||
221
- value.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
222
- value.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
223
- new Set(value).size !== value.length
226
+ normalizedTargets.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
227
+ normalizedTargets.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
228
+ new Set(normalizedTargets).size !== normalizedTargets.length
224
229
  ) {
225
230
  return planError('Protocol V2 firmware update targets are invalid');
226
231
  }
227
- return [...value] as FirmwareUpdateV4Target[];
232
+ return normalizedTargets as FirmwareUpdateV4Target[];
228
233
  };
229
234
 
230
235
  const assertExactKeys = (
@@ -323,8 +328,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
323
328
  }
324
329
  assertExactKeys(
325
330
  artifact,
326
- ['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
327
- ['logicalName', 'targetVersion']
331
+ ['artifactId', 'role', 'target', 'url', 'container'],
332
+ ['logicalName', 'expectedSize', 'expectedSha256', 'targetVersion']
328
333
  );
329
334
  const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
330
335
  if (artifactIds.has(artifactId)) {
@@ -343,15 +348,25 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
343
348
  if (artifact.logicalName !== undefined) {
344
349
  assertBoundedString(artifact.logicalName, 'logical name', 256);
345
350
  }
346
- if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
351
+ if (
352
+ artifact.expectedSize !== undefined &&
353
+ (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
354
+ ) {
347
355
  return planError('Firmware update plan artifact size is invalid');
348
356
  }
349
357
  if (
350
- typeof artifact.expectedSha256 !== 'string' ||
351
- !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
358
+ artifact.expectedSha256 !== undefined &&
359
+ (typeof artifact.expectedSha256 !== 'string' ||
360
+ !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
352
361
  ) {
353
362
  return planError('Firmware update plan artifact digest is invalid');
354
363
  }
364
+ if (
365
+ plan.executor === 'v4' &&
366
+ (artifact.expectedSize === undefined || artifact.expectedSha256 === undefined)
367
+ ) {
368
+ return planError('Protocol V2 firmware artifact integrity metadata is invalid');
369
+ }
355
370
  if (artifact.targetVersion !== undefined) {
356
371
  assertBoundedString(artifact.targetVersion, 'target version', 64);
357
372
  }
@@ -639,47 +654,6 @@ const finalizeFirmwareUpdatePlan = ({
639
654
  });
640
655
  };
641
656
 
642
- export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
643
- artifactId: string;
644
- target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
645
- container: 'raw' | 'zip';
646
- logicalName: string;
647
- expectedSize: number;
648
- expectedSha256: string;
649
- targetVersion?: string;
650
- };
651
-
652
- export const buildProtocolV2LocalFirmwareUpdatePlan = ({
653
- features,
654
- firmwareType,
655
- platform,
656
- artifacts,
657
- }: {
658
- features: Features;
659
- firmwareType: EFirmwareType;
660
- platform: FirmwareUpdatePlatform;
661
- artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
662
- }): FirmwareUpdatePlan => {
663
- if (artifacts.length === 0) {
664
- return planError('Protocol V2 local firmware plan has no artifacts');
665
- }
666
- const plan = finalizeFirmwareUpdatePlan({
667
- features,
668
- firmwareType,
669
- platform,
670
- artifacts: artifacts.map(artifact => ({
671
- ...artifact,
672
- role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
673
- url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
674
- })),
675
- targetsToUpdate: artifacts.map(artifact => artifact.target),
676
- });
677
- if (plan.executor !== 'v4') {
678
- return planError('Protocol V2 local firmware plan requires executor v4');
679
- }
680
- return plan;
681
- };
682
-
683
657
  export const buildProtocolV2FirmwareUpdatePlan = ({
684
658
  features,
685
659
  firmwareType,
@@ -817,13 +791,10 @@ export const buildFirmwareUpdatePlan = ({
817
791
  } else {
818
792
  if (isUpgrade(bootloader) || forcedTargets.has('bootloader')) {
819
793
  const bootloaderRelease = asRelease(bootloader);
820
- const integrity = requireArtifactIntegrity(
821
- {
822
- size: bootloaderRelease?.bootloaderExpectedSize,
823
- sha256: bootloaderRelease?.bootloaderFingerprint,
824
- },
825
- 'Bootloader release'
826
- );
794
+ const integrity = asIntegrity({
795
+ size: bootloaderRelease?.bootloaderExpectedSize,
796
+ sha256: bootloaderRelease?.bootloaderFingerprint,
797
+ });
827
798
  artifacts.push({
828
799
  artifactId: 'bootloader',
829
800
  role: 'bootloader',
@@ -845,10 +816,10 @@ export const buildFirmwareUpdatePlan = ({
845
816
  });
846
817
  }
847
818
  if (shouldUpdateFirmware) {
848
- const integrity = requireArtifactIntegrity(
849
- { size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
850
- 'Firmware release'
851
- );
819
+ const integrity = asIntegrity({
820
+ size: firmwareRelease.expectedSize,
821
+ sha256: firmwareRelease.fingerprint,
822
+ });
852
823
  artifacts.push({
853
824
  artifactId: 'firmware',
854
825
  role: 'firmware',
@@ -868,19 +839,16 @@ export const buildFirmwareUpdatePlan = ({
868
839
  platform,
869
840
  });
870
841
  if (resourceUrl) {
871
- const integrity = requireArtifactIntegrity(
872
- {
873
- size:
874
- resourceUrl === asString(firmwareRelease.fullResource)
875
- ? firmwareRelease.fullResourceExpectedSize
876
- : firmwareRelease.resourceExpectedSize,
877
- sha256:
878
- resourceUrl === asString(firmwareRelease.fullResource)
879
- ? firmwareRelease.fullResourceFingerprint
880
- : firmwareRelease.resourceFingerprint,
881
- },
882
- 'Legacy resource archive'
883
- );
842
+ const integrity = asIntegrity({
843
+ size:
844
+ resourceUrl === asString(firmwareRelease.fullResource)
845
+ ? firmwareRelease.fullResourceExpectedSize
846
+ : firmwareRelease.resourceExpectedSize,
847
+ sha256:
848
+ resourceUrl === asString(firmwareRelease.fullResource)
849
+ ? firmwareRelease.fullResourceFingerprint
850
+ : firmwareRelease.resourceFingerprint,
851
+ });
884
852
  artifacts.push({
885
853
  artifactId: 'resource',
886
854
  role: 'resource',
@@ -894,13 +862,10 @@ export const buildFirmwareUpdatePlan = ({
894
862
  }
895
863
  if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
896
864
  const bleRelease = asRelease(ble);
897
- const integrity = requireArtifactIntegrity(
898
- {
899
- size: bleRelease?.expectedSize,
900
- sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
901
- },
902
- 'BLE release'
903
- );
865
+ const integrity = asIntegrity({
866
+ size: bleRelease?.expectedSize,
867
+ sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
868
+ });
904
869
  artifacts.push({
905
870
  artifactId: 'ble',
906
871
  role: 'ble',
@@ -916,6 +881,14 @@ export const buildFirmwareUpdatePlan = ({
916
881
  targetsToUpdate = [...new Set(artifacts.map(artifact => artifact.target))];
917
882
  }
918
883
 
884
+ if (
885
+ executor !== 'v4' &&
886
+ targetsToUpdate.includes('resource') &&
887
+ !targetsToUpdate.includes('firmware')
888
+ ) {
889
+ planError('Legacy resource updates require a firmware target');
890
+ }
891
+
919
892
  assertForcedTargetsRepresented({
920
893
  executor,
921
894
  forcedTargets: validatedForceTargets,
@@ -71,6 +71,13 @@ export const getFirmwareUpdateResourceName = (value: unknown): string => {
71
71
  return resourceName;
72
72
  };
73
73
 
74
+ const getPreparedEntryIdentity = (entryName: unknown): string => {
75
+ getFirmwareUpdateResourceName(entryName);
76
+ // Protocol V2 resource ZIPs can ship the same basename from different
77
+ // directories, such as rom/params.okpkg and bootloader/params.okpkg.
78
+ return (entryName as string).toLowerCase();
79
+ };
80
+
74
81
  const assertPreparedEntry = (value: unknown): FirmwareUpdatePreparedEntry => {
75
82
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
76
83
  return preparedPlanError('Firmware prepared plan entry must be an object');
@@ -141,11 +148,8 @@ const assertPreparedArtifacts = ({
141
148
  }
142
149
  if (
143
150
  materializedEntries &&
144
- new Set(
145
- materializedEntries.map(entry =>
146
- getFirmwareUpdateResourceName(entry.entryName).toLowerCase()
147
- )
148
- ).size !== materializedEntries.length
151
+ new Set(materializedEntries.map(entry => getPreparedEntryIdentity(entry.entryName))).size !==
152
+ materializedEntries.length
149
153
  ) {
150
154
  return preparedPlanError('Firmware prepared plan contains duplicate entry names');
151
155
  }
@@ -281,7 +285,7 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
281
285
  const materializedEntryNames =
282
286
  artifact.materializedEntries?.map(entry => {
283
287
  assertPreparedEntry(entry);
284
- return getFirmwareUpdateResourceName(entry.entryName).toLowerCase();
288
+ return getPreparedEntryIdentity(entry.entryName);
285
289
  }) ?? [];
286
290
  if (new Set(materializedEntryNames).size !== materializedEntryNames.length) {
287
291
  return preparedPlanError('Firmware prepared plan contains duplicate entry names');
@@ -321,23 +325,34 @@ export const getFirmwareUpdatePreparedRawArtifact = ({
321
325
 
322
326
  /**
323
327
  * Identity observed on the live device while a degraded recovery plan runs, keyed by
324
- * the plan's opaque lease. Bootloader recovery starts with no serial and the device
325
- * reports one only after the firmware phase reboots it, so the later phases of the
326
- * SAME workflow must be allowed to continue — but only for the device that first
327
- * answered, never for a second one that appears mid-recovery.
328
+ * the approved prepared-plan digest. Bootloader recovery starts with no serial and
329
+ * the device reports one only after the firmware phase reboots it, so the later
330
+ * phases of the SAME workflow must be allowed to continue — but only for the device
331
+ * that first answered, never for a second one that appears mid-recovery.
328
332
  */
329
333
  const degradedPlanIdentityPins = new Map<string, string>();
330
334
  /** Keep the pin map from growing without bound across many workflows. */
331
335
  const DEGRADED_PLAN_IDENTITY_PIN_LIMIT = 32;
332
336
 
333
- const pinDegradedPlanIdentity = (leaseRef: string, deviceIdentity: string) => {
334
- if (degradedPlanIdentityPins.size >= DEGRADED_PLAN_IDENTITY_PIN_LIMIT) {
337
+ const pinDegradedPlanIdentity = (preparedPlanDigest: string, deviceIdentity: string) => {
338
+ const key = preparedPlanDigest.toLowerCase();
339
+ if (
340
+ !degradedPlanIdentityPins.has(key) &&
341
+ degradedPlanIdentityPins.size >= DEGRADED_PLAN_IDENTITY_PIN_LIMIT
342
+ ) {
335
343
  const oldest = degradedPlanIdentityPins.keys().next();
336
344
  if (!oldest.done) {
337
345
  degradedPlanIdentityPins.delete(oldest.value);
338
346
  }
339
347
  }
340
- degradedPlanIdentityPins.set(leaseRef, deviceIdentity);
348
+ degradedPlanIdentityPins.set(key, deviceIdentity);
349
+ };
350
+
351
+ /** Release recovery identity state when its digest-bound host workflow ends. */
352
+ export const clearFirmwareUpdatePreparedPlanDeviceIdentityPin = (
353
+ preparedPlanDigest: string
354
+ ): void => {
355
+ degradedPlanIdentityPins.delete(preparedPlanDigest.toLowerCase());
341
356
  };
342
357
 
343
358
  export const assertFirmwareUpdatePreparedPlanDeviceIdentity = ({
@@ -364,7 +379,9 @@ export const assertFirmwareUpdatePreparedPlanDeviceIdentity = ({
364
379
  if (deviceModel === undefined || preparedPlan.deviceModel !== deviceModel) {
365
380
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
366
381
  }
367
- const pinnedIdentity = degradedPlanIdentityPins.get(preparedPlan.leaseRef);
382
+ const pinnedIdentity = degradedPlanIdentityPins.get(
383
+ preparedPlan.preparedPlanDigest.toLowerCase()
384
+ );
368
385
  if (!deviceIdentity) {
369
386
  // V4 设备可能尚未写入序列号;V2 则只允许 bootloader 恢复流程缺少身份。
370
387
  if (preparedPlan.executor !== 'v4' && bootloaderMode !== true) {
@@ -375,7 +392,7 @@ export const assertFirmwareUpdatePreparedPlanDeviceIdentity = ({
375
392
  // The recovered device now reports a serial: bind this recovery to it, and hold
376
393
  // every later phase to the same one.
377
394
  if (!pinnedIdentity) {
378
- pinDegradedPlanIdentity(preparedPlan.leaseRef, deviceIdentity);
395
+ pinDegradedPlanIdentity(preparedPlan.preparedPlanDigest, deviceIdentity);
379
396
  return preparedPlan;
380
397
  }
381
398
  if (pinnedIdentity !== deviceIdentity) {
@@ -77,11 +77,16 @@ export async function loadProtocolV2FirmwareReleaseContext({
77
77
  }
78
78
 
79
79
  const firmwareType = firmwareTypeParam ?? state.identity.firmwareType ?? EFirmwareType.Universal;
80
+ const normalizedFeatures: Features = {
81
+ ...features,
82
+ deviceType: state.identity.deviceType,
83
+ firmwareType,
84
+ };
80
85
  return {
81
86
  state: state as ProtocolV2FirmwareReleaseContext['state'],
82
- features,
87
+ features: normalizedFeatures,
83
88
  firmwareType,
84
- release: DataManager.getFirmwareLatestRelease(features, firmwareType),
89
+ release: DataManager.getFirmwareLatestRelease(normalizedFeatures, firmwareType),
85
90
  };
86
91
  }
87
92
 
@@ -0,0 +1,85 @@
1
+ import { Buffer } from 'buffer';
2
+ import { decode as decodeJpeg } from 'jpeg-js';
3
+
4
+ import { invalidParameter } from './filesystemValidation';
5
+
6
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
7
+ const JPEG_CONTAINER_OVERHEAD_BYTES = 64 * 1024;
8
+ const JPEG_MAX_MEMORY_USAGE_IN_MB = 32;
9
+ const JPEG_MAX_RESOLUTION_IN_MP = 1;
10
+
11
+ export function decodeCanonicalBase64({
12
+ value,
13
+ parameterName,
14
+ maxBytes,
15
+ }: {
16
+ value: unknown;
17
+ parameterName: string;
18
+ maxBytes: number;
19
+ }): Uint8Array {
20
+ if (typeof value !== 'string' || value.length === 0) {
21
+ throw invalidParameter(`Parameter [${parameterName}] must be a non-empty Base64 string.`);
22
+ }
23
+ if (value.length > Math.ceil(maxBytes / 3) * 4) {
24
+ throw invalidParameter(`Parameter [${parameterName}] exceeds the maximum supported size.`);
25
+ }
26
+ if (value.length % 4 !== 0 || !BASE64_PATTERN.test(value)) {
27
+ throw invalidParameter(`Parameter [${parameterName}] must use canonical Base64 encoding.`);
28
+ }
29
+
30
+ const decoded = Buffer.from(value, 'base64');
31
+ if (decoded.byteLength === 0 || decoded.byteLength > maxBytes) {
32
+ throw invalidParameter(`Parameter [${parameterName}] exceeds the maximum supported size.`);
33
+ }
34
+ if (decoded.toString('base64') !== value) {
35
+ throw invalidParameter(`Parameter [${parameterName}] must use canonical Base64 encoding.`);
36
+ }
37
+ return Uint8Array.from(decoded);
38
+ }
39
+
40
+ export function decodeJpegBase64ToRgba({
41
+ jpegBase64,
42
+ parameterName,
43
+ expectedWidth,
44
+ expectedHeight,
45
+ }: {
46
+ jpegBase64: unknown;
47
+ parameterName: string;
48
+ expectedWidth: number;
49
+ expectedHeight: number;
50
+ }): { width: number; height: number; data: Uint8Array } {
51
+ const jpegBytes = decodeCanonicalBase64({
52
+ value: jpegBase64,
53
+ parameterName,
54
+ maxBytes: expectedWidth * expectedHeight * 8 + JPEG_CONTAINER_OVERHEAD_BYTES,
55
+ });
56
+ if (jpegBytes[0] !== 0xff || jpegBytes[1] !== 0xd8) {
57
+ throw invalidParameter(`Parameter [${parameterName}] must contain a JPEG image.`);
58
+ }
59
+
60
+ let decoded: { width: number; height: number; data: Uint8Array };
61
+ try {
62
+ decoded = decodeJpeg(jpegBytes, {
63
+ useTArray: true,
64
+ formatAsRGBA: true,
65
+ tolerantDecoding: false,
66
+ maxResolutionInMP: JPEG_MAX_RESOLUTION_IN_MP,
67
+ maxMemoryUsageInMB: JPEG_MAX_MEMORY_USAGE_IN_MB,
68
+ });
69
+ } catch {
70
+ throw invalidParameter(`Parameter [${parameterName}] must contain a valid JPEG image.`);
71
+ }
72
+
73
+ if (decoded.width !== expectedWidth || decoded.height !== expectedHeight) {
74
+ throw invalidParameter(
75
+ `Parameter [${parameterName}] must contain a ${expectedWidth}x${expectedHeight} JPEG image.`
76
+ );
77
+ }
78
+ const expectedLength = expectedWidth * expectedHeight * 4;
79
+ if (decoded.data.byteLength !== expectedLength) {
80
+ throw invalidParameter(
81
+ `Decoded parameter [${parameterName}] must contain ${expectedLength} RGBA bytes.`
82
+ );
83
+ }
84
+ return decoded;
85
+ }
@@ -25,7 +25,7 @@ export default class DeviceFactoryInfoSet extends BaseMethod<DeviceFactoryInfoSe
25
25
  info: {
26
26
  version: this.params.version,
27
27
  serial_number: this.params.serial_number,
28
- burn_in_completed: this.params.burn_in_completed,
28
+ factory_burn_in_completed: this.params.burn_in_completed,
29
29
  factory_test_completed: this.params.factory_test_completed,
30
30
  manufacture_time: this.params.manufacture_time,
31
31
  },