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

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 (38) hide show
  1. package/__tests__/firmware-update/device-update-bootloader.test.ts +8 -1
  2. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +106 -2
  3. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  4. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
  5. package/__tests__/firmware-update/firmware-update-plan.test.ts +101 -0
  6. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  7. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  8. package/__tests__/protocol-v2.test.ts +131 -0
  9. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  14. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  15. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  18. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  19. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -3
  21. package/dist/index.js +359 -157
  22. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  23. package/dist/types/api/firmwareUpdate.d.ts +1 -1
  24. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  25. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  26. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/api/FirmwareUpdateV2.ts +95 -72
  29. package/src/api/FirmwareUpdateV3.ts +74 -51
  30. package/src/api/FirmwareUpdateV4.ts +72 -7
  31. package/src/api/device/DeviceUpdateBootloader.ts +17 -12
  32. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  33. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
  34. package/src/api/firmware/FirmwareUpdatePlan.ts +70 -48
  35. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +17 -0
  36. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  37. package/src/types/api/firmwareUpdate.ts +2 -1
  38. package/src/types/api/firmwareUpdatePlan.ts +2 -2
@@ -9,6 +9,9 @@ import {
9
9
  jest.mock('../../src/api/firmware/FirmwareUpdatePreparedPlan', () => ({
10
10
  assertFirmwareUpdatePreparedPlanBinding: jest.fn(),
11
11
  assertFirmwareUpdatePreparedPlanDeviceIdentity: jest.fn(),
12
+ getFirmwareUpdatePreparedRawArtifact: jest.fn(
13
+ ({ preparedPlan }: { preparedPlan: { artifacts: unknown[] } }) => preparedPlan.artifacts[0]
14
+ ),
12
15
  validateFirmwareUpdatePreparedPlan: jest.fn((preparedPlan: unknown) => preparedPlan),
13
16
  }));
14
17
 
@@ -47,7 +50,11 @@ const createMethod = () => {
47
50
  method: 'deviceUpdateBootloader',
48
51
  artifact,
49
52
  hostBindingGeneration,
50
- preparedPlan: { preparedPlanDigest: 'a'.repeat(64) },
53
+ preparedPlan: {
54
+ preparedPlanDigest: 'a'.repeat(64),
55
+ firmwareType: EFirmwareType.Universal,
56
+ artifacts: [{ artifact }],
57
+ },
51
58
  },
52
59
  });
53
60
  (method as any).device = {
@@ -207,7 +207,40 @@ describe('prepared firmware host digest binding', () => {
207
207
  },
208
208
  });
209
209
 
210
- expect(() => method.init()).toThrow('Prepared firmware plans require a prepared artifact');
210
+ expect(() => method.init()).toThrow(
211
+ 'Prepared firmware plans cannot be combined with legacy firmware inputs'
212
+ );
213
+ });
214
+
215
+ test('FirmwareUpdateV2 derives its component artifact from the digest-bound plan', () => {
216
+ const { artifact, preparedPlan } = createPreparedPlan({
217
+ executor: 'v2',
218
+ target: 'firmware',
219
+ });
220
+ const artifactReader = {
221
+ open: jest.fn(),
222
+ read: jest.fn(),
223
+ close: jest.fn(),
224
+ };
225
+ const hostBindingGeneration = registerFirmwareUpdateHostBinding({
226
+ preparedPlanDigest: preparedPlan.preparedPlanDigest,
227
+ artifactReader,
228
+ });
229
+ const method = new FirmwareUpdateV2({
230
+ id: 1,
231
+ payload: {
232
+ method: 'firmwareUpdateV2',
233
+ platform: 'desktop',
234
+ updateType: 'firmware',
235
+ preparedPlan,
236
+ hostBindingGeneration,
237
+ },
238
+ });
239
+
240
+ expect(() => method.init()).not.toThrow();
241
+ expect((method as unknown as { params: { artifact: unknown } }).params.artifact).toEqual(
242
+ artifact
243
+ );
211
244
  });
212
245
 
213
246
  test('FirmwareUpdateV3 rejects a host registered for another prepared plan', () => {
@@ -277,6 +310,37 @@ describe('prepared firmware host digest binding', () => {
277
310
  );
278
311
  });
279
312
 
313
+ test('FirmwareUpdateV3 derives its component artifacts from the digest-bound plan', () => {
314
+ const { artifact, preparedPlan } = createPreparedPlan({
315
+ executor: 'v3',
316
+ target: 'firmware',
317
+ });
318
+ const artifactReader = {
319
+ open: jest.fn(),
320
+ read: jest.fn(),
321
+ close: jest.fn(),
322
+ };
323
+ const hostBindingGeneration = registerFirmwareUpdateHostBinding({
324
+ preparedPlanDigest: preparedPlan.preparedPlanDigest,
325
+ artifactReader,
326
+ });
327
+ const method = new FirmwareUpdateV3({
328
+ id: 1,
329
+ payload: {
330
+ method: 'firmwareUpdateV3',
331
+ platform: 'desktop',
332
+ preparedPlan,
333
+ hostBindingGeneration,
334
+ },
335
+ });
336
+
337
+ expect(() => method.init()).not.toThrow();
338
+ expect(
339
+ (method as unknown as { params: { artifacts: { firmware: unknown } } }).params.artifacts
340
+ .firmware
341
+ ).toEqual(artifact);
342
+ });
343
+
280
344
  test('DeviceUpdateBootloader rejects a host registered for another prepared plan', async () => {
281
345
  const { artifact, preparedPlan } = createPreparedPlan({
282
346
  executor: 'v2',
@@ -349,7 +413,47 @@ describe('prepared firmware host digest binding', () => {
349
413
  method.device = { features: undefined } as any;
350
414
 
351
415
  await expect(method.run()).rejects.toThrow(
352
- 'Prepared bootloader plans require exactly one prepared artifact'
416
+ 'Prepared bootloader plans cannot be combined with a legacy binary'
417
+ );
418
+ });
419
+
420
+ test('DeviceUpdateBootloader derives its artifact from the digest-bound plan', async () => {
421
+ const { artifact, preparedPlan } = createPreparedPlan({
422
+ executor: 'v2',
423
+ target: 'bootloader',
424
+ });
425
+ const artifactReader = {
426
+ open: jest.fn().mockResolvedValue({ readerId: 'bootloader-reader', size: artifact.size }),
427
+ read: jest.fn(),
428
+ close: jest.fn().mockResolvedValue(undefined),
429
+ };
430
+ const hostBindingGeneration = registerFirmwareUpdateHostBinding({
431
+ preparedPlanDigest: preparedPlan.preparedPlanDigest,
432
+ artifactReader,
433
+ });
434
+ const method = new DeviceUpdateBootloader({
435
+ id: 1,
436
+ payload: {
437
+ method: 'deviceUpdateBootloader',
438
+ preparedPlan,
439
+ hostBindingGeneration,
440
+ },
441
+ });
442
+ method.device = {
443
+ features: {
444
+ deviceType: EDeviceType.Classic1s,
445
+ serialNo: 'classic-device-id',
446
+ },
447
+ getCurrentDeviceType: () => EDeviceType.Touch,
448
+ } as any;
449
+ const updateTouchBootloader = jest
450
+ .spyOn(method, 'updateTouchBootloader')
451
+ .mockResolvedValue(true);
452
+
453
+ await expect(method.run()).resolves.toBe(true);
454
+ expect(artifactReader.open).toHaveBeenCalledWith({ artifactRef: artifact.artifactRef });
455
+ expect(updateTouchBootloader).toHaveBeenCalledWith(
456
+ expect.objectContaining({ firmwareType: preparedPlan.firmwareType })
353
457
  );
354
458
  });
355
459
 
@@ -0,0 +1,150 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+ import { EFirmwareType } from '@onekeyfe/hd-shared';
4
+ import JSZip from 'jszip';
5
+
6
+ import { readVerifiedPreparedResourceArchive } from '../../src/api/firmware/FirmwarePreparedResourceArchive';
7
+ import { prepareFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
8
+ import { digestFirmwareUpdateContract } from '../../src/api/firmware/FirmwareUpdatePlan';
9
+
10
+ import type {
11
+ FirmwareArtifactReader,
12
+ FirmwareArtifactReference,
13
+ } from '../../src/types/api/firmwareUpdate';
14
+ import type { FirmwareUpdatePlan } from '../../src/types/api/firmwareUpdatePlan';
15
+
16
+ jest.mock('../../src/data/config', () => ({
17
+ getSDKVersion: jest.fn(() => '1.0.0'),
18
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
19
+ }));
20
+
21
+ const createReference = (artifactRef: string, binary: ArrayBuffer): FirmwareArtifactReference => ({
22
+ artifactRef,
23
+ size: binary.byteLength,
24
+ sha256: bytesToHex(sha256(new Uint8Array(binary))),
25
+ });
26
+
27
+ const createReader = (artifacts: Map<string, ArrayBuffer>): FirmwareArtifactReader => {
28
+ const opened = new Map<string, ArrayBuffer>();
29
+ let sequence = 0;
30
+ return {
31
+ open({ artifactRef }) {
32
+ const binary = artifacts.get(artifactRef);
33
+ if (!binary) throw new Error(`missing artifact: ${artifactRef}`);
34
+ sequence += 1;
35
+ const readerId = `reader-${sequence}`;
36
+ opened.set(readerId, binary);
37
+ return Promise.resolve({ readerId, size: binary.byteLength });
38
+ },
39
+ read({ readerId, offset, length }) {
40
+ const binary = opened.get(readerId);
41
+ if (!binary) throw new Error(`missing reader: ${readerId}`);
42
+ const data = binary.slice(offset, offset + length);
43
+ return Promise.resolve({
44
+ data,
45
+ bytesRead: data.byteLength,
46
+ eof: offset + length === binary.byteLength,
47
+ });
48
+ },
49
+ close({ readerId }) {
50
+ opened.delete(readerId);
51
+ return Promise.resolve();
52
+ },
53
+ };
54
+ };
55
+
56
+ const createPreparedResourcePlan = ({
57
+ executor,
58
+ archiveBinary,
59
+ entryBinary,
60
+ }: {
61
+ executor: 'v2' | 'v3';
62
+ archiveBinary: ArrayBuffer;
63
+ entryBinary: ArrayBuffer;
64
+ }) => {
65
+ const archiveArtifact = createReference('resource-archive', archiveBinary);
66
+ const entryArtifact = createReference('resource-entry', entryBinary);
67
+ const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
68
+ schemaVersion: 2,
69
+ executor,
70
+ deviceIdentity: `${executor}-device`,
71
+ deviceModel: executor === 'v2' ? 'TOUCH' : 'PRO',
72
+ firmwareType: EFirmwareType.Universal,
73
+ platform: 'desktop',
74
+ artifacts: [
75
+ {
76
+ artifactId: 'resource',
77
+ role: 'resource',
78
+ target: 'resource',
79
+ url: 'https://firmware.onekey.so/resource.zip',
80
+ container: 'zip',
81
+ expectedSize: archiveArtifact.size,
82
+ expectedSha256: archiveArtifact.sha256,
83
+ },
84
+ ],
85
+ targetsToUpdate: ['resource'],
86
+ };
87
+ const plan: FirmwareUpdatePlan = {
88
+ ...planWithoutDigest,
89
+ planDigest: digestFirmwareUpdateContract(planWithoutDigest),
90
+ };
91
+ return {
92
+ archiveArtifact,
93
+ preparedPlan: prepareFirmwareUpdatePlan({
94
+ plan,
95
+ leaseRef: `${executor}-resource-lease`,
96
+ artifacts: [
97
+ {
98
+ artifactId: 'resource',
99
+ artifact: archiveArtifact,
100
+ materializedEntries: [{ entryName: 'images/logo.bin', artifact: entryArtifact }],
101
+ },
102
+ ],
103
+ }),
104
+ };
105
+ };
106
+
107
+ describe.each(['v2', 'v3'] as const)('%s prepared resource archive binding', executor => {
108
+ test('uses canonical bytes extracted from the approved ZIP', async () => {
109
+ const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
110
+ const zip = new JSZip();
111
+ zip.file('images/logo.bin', entryBinary);
112
+ const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
113
+ const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
114
+ executor,
115
+ archiveBinary,
116
+ entryBinary,
117
+ });
118
+
119
+ const result = await readVerifiedPreparedResourceArchive({
120
+ preparedPlan,
121
+ reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
122
+ });
123
+
124
+ expect(result).toHaveLength(1);
125
+ expect(result[0].entryName).toBe('logo.bin');
126
+ expect(new Uint8Array(result[0].binary)).toEqual(new Uint8Array(entryBinary));
127
+ });
128
+
129
+ test('rejects entries that are not derived from the approved ZIP', async () => {
130
+ const approvedEntry = Uint8Array.from([1, 2, 3, 4]).buffer;
131
+ const substitutedEntry = Uint8Array.from([9, 9, 9, 9]).buffer;
132
+ const zip = new JSZip();
133
+ zip.file('images/logo.bin', approvedEntry);
134
+ const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
135
+ const { archiveArtifact, preparedPlan } = createPreparedResourcePlan({
136
+ executor,
137
+ archiveBinary,
138
+ entryBinary: substitutedEntry,
139
+ });
140
+
141
+ await expect(
142
+ readVerifiedPreparedResourceArchive({
143
+ preparedPlan,
144
+ reader: createReader(new Map([[archiveArtifact.artifactRef, archiveBinary]])),
145
+ })
146
+ ).rejects.toMatchObject({
147
+ params: { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' },
148
+ });
149
+ });
150
+ });
@@ -1,8 +1,11 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
1
3
  import FirmwareUpdate from '../../src/api/FirmwareUpdate';
2
4
  import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
3
5
  import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
4
6
  import { BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS } from '../../src/api/firmware/FirmwareUpdateBaseMethod';
5
7
  import { DataManager } from '../../src/data-manager';
8
+ import { DevicePool } from '../../src/device/DevicePool';
6
9
 
7
10
  import type { Device } from '../../src/device/Device';
8
11
 
@@ -12,6 +15,7 @@ jest.mock('../../src/data/config', () => ({
12
15
  }));
13
16
 
14
17
  const BLE_ID = 'ble-device';
18
+ const WEBUSB_ID = 'webusb-device';
15
19
 
16
20
  const flush = () =>
17
21
  new Promise(resolve => {
@@ -198,3 +202,111 @@ describe.each([
198
202
  expect(maxActiveProbes).toBe(1);
199
203
  });
200
204
  });
205
+
206
+ describe.each([
207
+ [
208
+ 'FirmwareUpdateV2',
209
+ () =>
210
+ new FirmwareUpdateV2({
211
+ id: 1,
212
+ payload: { method: 'firmwareUpdateV2', connectId: WEBUSB_ID },
213
+ }),
214
+ ],
215
+ [
216
+ 'FirmwareUpdateV3 (base method)',
217
+ () =>
218
+ new FirmwareUpdateV3({
219
+ id: 1,
220
+ payload: { method: 'firmwareUpdateV3', connectId: WEBUSB_ID },
221
+ }),
222
+ ],
223
+ ])('%s WebUSB bootloader authorization', (_name, buildMethod) => {
224
+ beforeEach(() => {
225
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
226
+ });
227
+
228
+ afterEach(() => {
229
+ jest.clearAllTimers();
230
+ jest.useRealTimers();
231
+ jest.restoreAllMocks();
232
+ });
233
+
234
+ test('pauses the reboot deadline while waiting for browser authorization', async () => {
235
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
236
+ jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
237
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
238
+
239
+ const method = buildMethod();
240
+ let permissionGranted = false;
241
+ const bootloaderDevice = {
242
+ isBootloader: jest.fn(() => true),
243
+ } as unknown as Device;
244
+ jest.spyOn(DevicePool, 'getDevices').mockImplementation(() =>
245
+ Promise.resolve({
246
+ devices: {},
247
+ deviceList: permissionGranted ? [bootloaderDevice] : [],
248
+ })
249
+ );
250
+
251
+ const commands = { disposed: true };
252
+ const updateFromCache = jest.fn();
253
+ method.device = {
254
+ originalDescriptor: {
255
+ id: WEBUSB_ID,
256
+ path: WEBUSB_ID,
257
+ protocolType: 'V1',
258
+ },
259
+ deviceConnector: {
260
+ enumerate: jest.fn(() =>
261
+ Promise.resolve({
262
+ descriptors: permissionGranted ? [{ path: 'bootloader-device' }] : [],
263
+ })
264
+ ),
265
+ },
266
+ commands,
267
+ updateFromCache,
268
+ getCurrentDeviceType: jest.fn(() => EDeviceType.Pro),
269
+ } as unknown as Device;
270
+ method.postTipMessage = jest.fn();
271
+
272
+ let resolvePrompt: (deviceId: string) => void = () => undefined;
273
+ const promptPromise = new Promise<string>(resolve => {
274
+ resolvePrompt = resolve;
275
+ });
276
+ const prompt = jest
277
+ .spyOn(method as any, '_promptDeviceInBootloaderForWebDevice')
278
+ .mockReturnValue(promptPromise);
279
+
280
+ method.checkDeviceToBootloader(WEBUSB_ID);
281
+ const resolved = jest.fn();
282
+ const rejected = jest.fn();
283
+ method.checkPromise?.promise.then(resolved, rejected);
284
+
285
+ for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
286
+ jest.advanceTimersByTime(500);
287
+ // eslint-disable-next-line no-await-in-loop
288
+ await flush();
289
+ if (prompt.mock.calls.length) break;
290
+ }
291
+
292
+ expect(prompt).toHaveBeenCalledTimes(1);
293
+
294
+ // Browser permission depends on a user gesture and must not consume the
295
+ // automatic reboot detection deadline.
296
+ for (let elapsed = 0; elapsed < 35000; elapsed += 500) {
297
+ jest.advanceTimersByTime(500);
298
+ // eslint-disable-next-line no-await-in-loop
299
+ await flush();
300
+ }
301
+ expect(rejected).not.toHaveBeenCalled();
302
+
303
+ permissionGranted = true;
304
+ resolvePrompt('bootloader-device');
305
+ await flush();
306
+ await flush();
307
+
308
+ expect(resolved).toHaveBeenCalledWith(true);
309
+ expect(updateFromCache).toHaveBeenCalledWith(bootloaderDevice);
310
+ expect(commands.disposed).toBe(false);
311
+ });
312
+ });
@@ -54,6 +54,8 @@ const createLegacyForceInput = (
54
54
  release: Record<string, unknown> = {
55
55
  url: 'https://firmware.onekey.so/pro/firmware.bin',
56
56
  version: [4, 21, 0],
57
+ expectedSize: 1024,
58
+ fingerprint: '1'.repeat(64),
57
59
  }
58
60
  ): BuildPlanInput => ({
59
61
  features: createFeatures({
@@ -172,6 +174,8 @@ describe('buildFirmwareUpdatePlan', () => {
172
174
  release: {
173
175
  url: 'https://firmware.onekey.so/pro/firmware.bin',
174
176
  version: [4, 0, 0],
177
+ expectedSize: 1024,
178
+ fingerprint: '1'.repeat(64),
175
179
  },
176
180
  },
177
181
  ble: noUpdate,
@@ -197,7 +201,11 @@ describe('buildFirmwareUpdatePlan', () => {
197
201
  release: {
198
202
  url: 'https://firmware.onekey.so/pro/firmware.bin',
199
203
  version: [4, 21, 0],
204
+ expectedSize: 1024,
205
+ fingerprint: '1'.repeat(64),
200
206
  resource: 'https://firmware.onekey.so/pro/resource.zip',
207
+ resourceExpectedSize: 4096,
208
+ resourceFingerprint: '1'.repeat(64),
201
209
  },
202
210
  },
203
211
  ble: {
@@ -205,6 +213,8 @@ describe('buildFirmwareUpdatePlan', () => {
205
213
  release: {
206
214
  webUpdate: 'https://firmware.onekey.so/pro/ble.bin',
207
215
  version: [2, 3, 7],
216
+ expectedSize: 512,
217
+ fingerprintWeb: '2'.repeat(64),
208
218
  },
209
219
  },
210
220
  bootloader: {
@@ -212,6 +222,8 @@ describe('buildFirmwareUpdatePlan', () => {
212
222
  release: {
213
223
  bootloaderResource: 'https://firmware.onekey.so/pro/bootloader.bin',
214
224
  bootloaderVersion: [2, 8, 4],
225
+ bootloaderExpectedSize: 768,
226
+ bootloaderFingerprint: '3'.repeat(64),
215
227
  },
216
228
  },
217
229
  forceUpdateTargets: ['firmware', 'resource', 'ble', 'bootloader'],
@@ -241,6 +253,8 @@ describe('buildFirmwareUpdatePlan', () => {
241
253
  release: {
242
254
  url: 'https://firmware.onekey.so/pro/firmware.bin',
243
255
  version: [4, 21, 0],
256
+ expectedSize: 1024,
257
+ fingerprint: '1'.repeat(64),
244
258
  resource: 'https://firmware.onekey.so/pro/resource.zip',
245
259
  },
246
260
  },
@@ -253,6 +267,32 @@ describe('buildFirmwareUpdatePlan', () => {
253
267
  expect(plan.targetsToUpdate).toEqual(['firmware']);
254
268
  });
255
269
 
270
+ test('does not approve an unexecutable V2 resource during bootloader recovery', () => {
271
+ const features = createFeatures({ deviceType: EDeviceType.Classic1s });
272
+ features.bootloaderMode = true;
273
+ const plan = buildFirmwareUpdatePlan({
274
+ features,
275
+ firmwareType: EFirmwareType.Universal,
276
+ platform: 'desktop',
277
+ firmware: {
278
+ status: 'none',
279
+ release: {
280
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
281
+ expectedSize: 1024,
282
+ fingerprint: '1'.repeat(64),
283
+ resource: 'https://firmware.onekey.so/classic/resource.zip',
284
+ resourceExpectedSize: 4096,
285
+ resourceFingerprint: '2'.repeat(64),
286
+ },
287
+ },
288
+ ble: noUpdate,
289
+ bootloader: noUpdate,
290
+ });
291
+
292
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(['firmware']);
293
+ expect(plan.targetsToUpdate).toEqual(['firmware']);
294
+ });
295
+
256
296
  test.each([
257
297
  {
258
298
  label: 'a non-array value',
@@ -276,6 +316,65 @@ describe('buildFirmwareUpdatePlan', () => {
276
316
  expectFirmwarePlanInvalid(() => buildFirmwareUpdatePlan(createLegacyForceInput(['resource'])));
277
317
  });
278
318
 
319
+ test('rejects a legacy resource archive without complete integrity metadata', () => {
320
+ expectFirmwarePlanInvalid(
321
+ () =>
322
+ buildFirmwareUpdatePlan(
323
+ createLegacyForceInput(['resource'], {
324
+ url: 'https://firmware.onekey.so/pro/firmware.bin',
325
+ resource: 'https://firmware.onekey.so/pro/resource.zip',
326
+ resourceExpectedSize: 4096,
327
+ })
328
+ ),
329
+ 'integrity metadata is invalid'
330
+ );
331
+ });
332
+
333
+ test.each(['firmware', 'ble', 'bootloader'] as const)(
334
+ 'rejects a remote legacy %s artifact without complete integrity metadata',
335
+ target => {
336
+ expectFirmwarePlanInvalid(
337
+ () =>
338
+ buildFirmwareUpdatePlan({
339
+ features: createFeatures({ deviceType: EDeviceType.Classic1s }),
340
+ firmwareType: EFirmwareType.Universal,
341
+ platform: 'desktop',
342
+ firmware:
343
+ target === 'firmware'
344
+ ? {
345
+ status: 'outdated',
346
+ release: {
347
+ url: 'https://firmware.onekey.so/classic/firmware.bin',
348
+ expectedSize: 1024,
349
+ },
350
+ }
351
+ : noUpdate,
352
+ ble:
353
+ target === 'ble'
354
+ ? {
355
+ status: 'outdated',
356
+ release: {
357
+ webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
358
+ expectedSize: 512,
359
+ },
360
+ }
361
+ : noUpdate,
362
+ bootloader:
363
+ target === 'bootloader'
364
+ ? {
365
+ status: 'outdated',
366
+ release: {
367
+ bootloaderResource: 'https://firmware.onekey.so/classic/bootloader.bin',
368
+ bootloaderExpectedSize: 768,
369
+ },
370
+ }
371
+ : noUpdate,
372
+ }),
373
+ 'integrity metadata is invalid'
374
+ );
375
+ }
376
+ );
377
+
279
378
  test.each(['ble', 'bootloader'] as const)(
280
379
  'rejects the unsupported Pro2 %s force target',
281
380
  forceTarget => {
@@ -358,6 +457,8 @@ describe('buildFirmwareUpdatePlan', () => {
358
457
  status: 'outdated',
359
458
  release: {
360
459
  url: 'https://firmware.onekey.so/classic/firmware.bin',
460
+ expectedSize: 1024,
461
+ fingerprint: '1'.repeat(64),
361
462
  },
362
463
  },
363
464
  ble: noUpdate,