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

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 (29) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +66 -61
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +1 -1
  3. package/__tests__/firmware-memory-host.test.ts +18 -4
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +1 -0
  5. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
  6. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  7. package/__tests__/firmware-update/firmware-update-plan.test.ts +61 -1
  8. package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
  9. package/__tests__/protocol-v2.test.ts +96 -0
  10. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  12. package/dist/api/FirmwareUpdateV4.d.ts +3 -1
  13. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  14. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -1
  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 +1 -0
  19. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  20. package/dist/index.js +207 -54
  21. package/package.json +4 -4
  22. package/src/api/CheckAllFirmwareRelease.ts +6 -1
  23. package/src/api/FirmwareUpdateV2.ts +123 -65
  24. package/src/api/FirmwareUpdateV4.ts +54 -8
  25. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  26. package/src/api/firmware/FirmwareMemoryHost.ts +4 -3
  27. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
  28. package/src/api/firmware/FirmwareUpdatePlan.ts +18 -5
  29. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +22 -9
@@ -328,15 +328,16 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
328
328
  status: 'required',
329
329
  resourceStatus: 'unknown',
330
330
  resourceArchive: resourceSource,
331
- targetsToUpdate: ['boot', 'app_v1'],
331
+ targetsToUpdate: ['boot', 'app_v1', 'resource'],
332
332
  firmwareUpdatePlan: {
333
333
  executor: 'v4',
334
334
  platform: 'web',
335
335
  artifacts: [
336
336
  { artifactId: 'component:boot', target: 'boot' },
337
337
  { artifactId: 'component:app_v1', target: 'app_v1' },
338
+ { artifactId: 'resource:archive', target: 'resource' },
338
339
  ],
339
- targetsToUpdate: ['boot', 'app_v1'],
340
+ targetsToUpdate: ['boot', 'app_v1', 'resource'],
340
341
  },
341
342
  firmware: {
342
343
  status: 'required',
@@ -377,7 +378,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
377
378
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
378
379
 
379
380
  await expect(method.run()).resolves.toMatchObject({
380
- targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03'],
381
+ targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03', 'resource'],
381
382
  firmwareUpdatePlan: {
382
383
  executor: 'v4',
383
384
  deviceIdentity: 'pro2-device-id',
@@ -387,8 +388,9 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
387
388
  { artifactId: 'component:app_v1', target: 'app_v1' },
388
389
  { artifactId: 'component:se02', target: 'se02' },
389
390
  { artifactId: 'component:se03', target: 'se03' },
391
+ { artifactId: 'resource:archive', target: 'resource' },
390
392
  ],
391
- targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03'],
393
+ targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03', 'resource'],
392
394
  },
393
395
  });
394
396
  });
@@ -582,12 +584,12 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
582
584
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
583
585
 
584
586
  await expect(method.run()).resolves.toMatchObject({
585
- targetsToUpdate: ['se01', 'se02'],
587
+ targetsToUpdate: ['se01', 'se02', 'resource'],
586
588
  firmwareUpdatePlan: {
587
589
  executor: 'v4',
588
590
  deviceModel: 'neo',
589
591
  platform: 'native',
590
- targetsToUpdate: ['se01', 'se02'],
592
+ targetsToUpdate: ['se01', 'se02', 'resource'],
591
593
  },
592
594
  });
593
595
  });
@@ -618,63 +620,66 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
618
620
  });
619
621
  });
620
622
 
621
- test('adds a Protocol V2 resource target only when explicitly forced', async () => {
622
- const currentRelease: IFirmwareReleaseInfo = {
623
- ...release,
624
- required: false,
625
- installOrder: ['applicationP1'],
626
- components: {
627
- applicationP1: {
628
- target: 'APPLICATION_P1',
629
- url: 'https://example.com/application-p1.okpkg',
630
- version: [1, 0, 0],
623
+ test.each(['resource', 'boot_resources'] as const)(
624
+ 'allows an explicitly forced Protocol V2 resource-only target through %s',
625
+ async forceTarget => {
626
+ const currentRelease: IFirmwareReleaseInfo = {
627
+ ...release,
628
+ required: false,
629
+ installOrder: ['applicationP1'],
630
+ components: {
631
+ applicationP1: {
632
+ target: 'APPLICATION_P1',
633
+ url: 'https://example.com/application-p1.okpkg',
634
+ version: [1, 0, 0],
635
+ },
631
636
  },
632
- },
633
- };
634
- const method = new CheckAllFirmwareRelease({
635
- id: 1,
636
- payload: {
637
- method: 'checkAllFirmwareRelease',
638
- platform: 'desktop',
639
- protocolV2ForceUpdateTargets: ['resource'],
640
- },
641
- });
642
- method.init();
643
- method.device = {
644
- isProtocolV2: () => true,
645
- features: {
646
- deviceType: 'pro2',
647
- serialNo: 'pro2-device-id',
648
- firmwareVersion: '1.0.0',
649
- },
650
- getDeviceState: jest.fn().mockResolvedValue({
651
- identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
652
- status: { mode: 'normal' },
653
- versions: { ...currentVersions, applicationP1: '1.0.0' },
654
- }),
655
- } as unknown as CheckAllFirmwareRelease['device'];
656
- jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(currentRelease);
657
- jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
637
+ };
638
+ const method = new CheckAllFirmwareRelease({
639
+ id: 1,
640
+ payload: {
641
+ method: 'checkAllFirmwareRelease',
642
+ platform: 'desktop',
643
+ protocolV2ForceUpdateTargets: [forceTarget],
644
+ },
645
+ });
646
+ method.init();
647
+ method.device = {
648
+ isProtocolV2: () => true,
649
+ features: {
650
+ deviceType: 'pro2',
651
+ serialNo: 'pro2-device-id',
652
+ firmwareVersion: '1.0.0',
653
+ },
654
+ getDeviceState: jest.fn().mockResolvedValue({
655
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
656
+ status: { mode: 'normal' },
657
+ versions: { ...currentVersions, applicationP1: '1.0.0' },
658
+ }),
659
+ } as unknown as CheckAllFirmwareRelease['device'];
660
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(currentRelease);
661
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
658
662
 
659
- await expect(method.run()).resolves.toMatchObject({
660
- status: 'valid',
661
- hasUpgrade: true,
662
- targetsToUpdate: ['resource'],
663
- firmwareUpdatePlan: {
664
- executor: 'v4',
665
- platform: 'desktop',
666
- artifacts: [
667
- {
668
- artifactId: 'resource:archive',
669
- target: 'resource',
670
- role: 'resourceBundle',
671
- container: 'zip',
672
- },
673
- ],
663
+ await expect(method.run()).resolves.toMatchObject({
664
+ status: 'valid',
665
+ hasUpgrade: true,
674
666
  targetsToUpdate: ['resource'],
675
- },
676
- });
677
- });
667
+ firmwareUpdatePlan: {
668
+ executor: 'v4',
669
+ platform: 'desktop',
670
+ artifacts: [
671
+ {
672
+ artifactId: 'resource:archive',
673
+ target: 'resource',
674
+ role: 'resourceBundle',
675
+ container: 'zip',
676
+ },
677
+ ],
678
+ targetsToUpdate: ['resource'],
679
+ },
680
+ });
681
+ }
682
+ );
678
683
 
679
684
  test('requests and compares firmware hashes only when explicitly enabled', async () => {
680
685
  const packageSet: IFirmwareReleaseInfo = {
@@ -752,7 +757,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
752
757
  protocol: 'V2',
753
758
  resourceStatus: 'unknown',
754
759
  resourceArchive: resourceSource,
755
- targetsToUpdate: ['boot', 'app_v1'],
760
+ targetsToUpdate: ['boot', 'app_v1', 'resource'],
756
761
  });
757
762
  expect(typedCall).not.toHaveBeenCalled();
758
763
  }
@@ -147,7 +147,7 @@ describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', dev
147
147
  deviceType,
148
148
  status: 'outdated',
149
149
  hasUpgrade: true,
150
- targetsToUpdate: ['boot', 'app_v1', 'coprocessor'],
150
+ targetsToUpdate: ['boot', 'app_v1', 'coprocessor', 'resource'],
151
151
  firmware: { status: 'outdated', shouldUpdate: true },
152
152
  ble: {
153
153
  status: 'outdated',
@@ -93,18 +93,32 @@ describe('prepareFirmwareUpdateV4MemoryHost', () => {
93
93
  const componentArtifact = host.preparedPlan.artifacts.find(
94
94
  artifact => artifact.target === 'boot'
95
95
  )?.artifact;
96
+ const resourceEntryArtifact = host.preparedPlan.artifacts.find(
97
+ artifact => artifact.target === 'resource'
98
+ )?.materializedEntries?.[0]?.artifact;
96
99
  expect(componentArtifact?.size).toBe(componentBinary.byteLength);
100
+ expect(resourceEntryArtifact?.size).toBe(manifestBinary.byteLength);
101
+ if (!artifactReader || !componentArtifact || !resourceEntryArtifact) {
102
+ throw new Error('Firmware memory host test setup is incomplete');
103
+ }
97
104
  new Uint8Array(componentBinary).fill(9);
98
- const opened = await artifactReader!.open({
99
- artifactRef: componentArtifact!.artifactRef,
105
+ const opened = await artifactReader.open({
106
+ artifactRef: componentArtifact.artifactRef,
100
107
  });
101
- const chunk = await artifactReader!.read({
108
+ const chunk = await artifactReader.read({
102
109
  readerId: opened.readerId,
103
110
  offset: 1,
104
111
  length: 2,
105
112
  });
106
113
  expect(Array.from(new Uint8Array(chunk.data))).toEqual([2, 3]);
107
- await artifactReader!.close({ readerId: opened.readerId });
114
+ await artifactReader.close({ readerId: opened.readerId });
115
+
116
+ // Materialized ZIP entries are receipts only. Execution opens the approved
117
+ // archive and re-derives these bytes, so the memory host must not retain a
118
+ // second readable copy of every expanded resource file.
119
+ expect(() => artifactReader.open({ artifactRef: resourceEntryArtifact.artifactRef })).toThrow(
120
+ 'Firmware memory artifact is unavailable'
121
+ );
108
122
 
109
123
  host.release();
110
124
  expect(unregisterFirmwareUpdateHostBinding).toHaveBeenCalledWith(7);
@@ -9,6 +9,7 @@ import {
9
9
  jest.mock('../../src/api/firmware/FirmwareUpdatePreparedPlan', () => ({
10
10
  assertFirmwareUpdatePreparedPlanBinding: jest.fn(),
11
11
  assertFirmwareUpdatePreparedPlanDeviceIdentity: jest.fn(),
12
+ clearFirmwareUpdatePreparedPlanDeviceIdentityPin: jest.fn(),
12
13
  getFirmwareUpdatePreparedRawArtifact: jest.fn(
13
14
  ({ preparedPlan }: { preparedPlan: { artifacts: unknown[] } }) => preparedPlan.artifacts[0]
14
15
  ),
@@ -231,7 +231,7 @@ describe.each([
231
231
  jest.restoreAllMocks();
232
232
  });
233
233
 
234
- test('pauses the reboot deadline while waiting for browser authorization', async () => {
234
+ test('resumes bounded polling after browser authorization returns an empty enumeration', async () => {
235
235
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
236
236
  jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
237
237
  jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
@@ -300,13 +300,76 @@ describe.each([
300
300
  }
301
301
  expect(rejected).not.toHaveBeenCalled();
302
302
 
303
- permissionGranted = true;
304
303
  resolvePrompt('bootloader-device');
305
304
  await flush();
306
305
  await flush();
307
306
 
307
+ // The chooser may resolve before the newly authorized device appears in the
308
+ // next WebUSB enumeration. That transient empty result must not strand the
309
+ // deferred check after its original interval and timeout were cleared.
310
+ expect(resolved).not.toHaveBeenCalled();
311
+ expect(rejected).not.toHaveBeenCalled();
312
+
313
+ permissionGranted = true;
314
+ for (let elapsed = 0; elapsed < 5000; elapsed += 500) {
315
+ jest.advanceTimersByTime(500);
316
+ // eslint-disable-next-line no-await-in-loop
317
+ await flush();
318
+ if (resolved.mock.calls.length) break;
319
+ }
320
+
308
321
  expect(resolved).toHaveBeenCalledWith(true);
309
322
  expect(updateFromCache).toHaveBeenCalledWith(bootloaderDevice);
310
323
  expect(commands.disposed).toBe(false);
311
324
  });
325
+
326
+ test('rejects after the fresh deadline when the authorized device never appears', async () => {
327
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
328
+ jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
329
+ jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(true);
330
+ jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({ devices: {}, deviceList: [] });
331
+
332
+ const method = buildMethod();
333
+ method.device = {
334
+ originalDescriptor: {
335
+ id: WEBUSB_ID,
336
+ path: WEBUSB_ID,
337
+ protocolType: 'V1',
338
+ },
339
+ deviceConnector: {
340
+ enumerate: jest.fn().mockResolvedValue({ descriptors: [] }),
341
+ },
342
+ commands: { disposed: true },
343
+ updateFromCache: jest.fn(),
344
+ getCurrentDeviceType: jest.fn(() => EDeviceType.Pro),
345
+ } as unknown as Device;
346
+ method.postTipMessage = jest.fn();
347
+ const prompt = jest
348
+ .spyOn(method as any, '_promptDeviceInBootloaderForWebDevice')
349
+ .mockResolvedValue('bootloader-device');
350
+
351
+ method.checkDeviceToBootloader(WEBUSB_ID);
352
+ const resolved = jest.fn();
353
+ const rejected = jest.fn();
354
+ method.checkPromise?.promise.then(resolved, rejected);
355
+
356
+ for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
357
+ jest.advanceTimersByTime(500);
358
+ // eslint-disable-next-line no-await-in-loop
359
+ await flush();
360
+ if (prompt.mock.calls.length) break;
361
+ }
362
+ expect(prompt).toHaveBeenCalledTimes(1);
363
+
364
+ for (let elapsed = 0; elapsed < 31000; elapsed += 500) {
365
+ jest.advanceTimersByTime(500);
366
+ // eslint-disable-next-line no-await-in-loop
367
+ await flush();
368
+ if (rejected.mock.calls.length) break;
369
+ }
370
+
371
+ expect(resolved).not.toHaveBeenCalled();
372
+ expect(rejected).toHaveBeenCalledTimes(1);
373
+ expect(prompt).toHaveBeenCalledTimes(1);
374
+ });
312
375
  });
@@ -1,6 +1,10 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
4
+ import {
5
+ registerFirmwareUpdateHostBinding,
6
+ unregisterFirmwareUpdateHostBinding,
7
+ } from '../../src/api/firmware/FirmwareHostBinding';
4
8
  import {
5
9
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
6
10
  prepareFirmwareUpdatePlan,
@@ -179,6 +183,19 @@ describe('firmware update plan identity in bootloader mode', () => {
179
183
  ],
180
184
  });
181
185
  };
186
+ const createBinding = (preparedPlanDigest: string) => ({
187
+ preparedPlanDigest,
188
+ artifactReader: {
189
+ open: () => Promise.resolve({ readerId: 'unused', size: 0 }),
190
+ read: () =>
191
+ Promise.resolve({
192
+ data: new ArrayBuffer(0),
193
+ bytesRead: 0,
194
+ eof: true,
195
+ }),
196
+ close: () => Promise.resolve(),
197
+ },
198
+ });
182
199
 
183
200
  test('accepts the phase that runs after the device reboots and reports a serial', () => {
184
201
  const preparedPlan = prepareRecoveryPlan();
@@ -250,6 +267,69 @@ describe('firmware update plan identity in bootloader mode', () => {
250
267
  })
251
268
  ).not.toThrow();
252
269
  });
270
+
271
+ test('releases the identity pin when the digest-bound host workflow ends', () => {
272
+ const preparedPlan = prepareRecoveryPlan();
273
+ const firstGeneration = registerFirmwareUpdateHostBinding(
274
+ createBinding(preparedPlan.preparedPlanDigest)
275
+ );
276
+ try {
277
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
278
+ preparedPlan,
279
+ deviceIdentity: 'CLA45F0023',
280
+ bootloaderMode: false,
281
+ deviceModel: String(EDeviceType.Classic1s),
282
+ });
283
+ } finally {
284
+ expect(unregisterFirmwareUpdateHostBinding(firstGeneration)).toBe(true);
285
+ }
286
+
287
+ const secondGeneration = registerFirmwareUpdateHostBinding(
288
+ createBinding(preparedPlan.preparedPlanDigest)
289
+ );
290
+ try {
291
+ expect(() =>
292
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
293
+ preparedPlan,
294
+ deviceIdentity: 'CLA99Z9999',
295
+ bootloaderMode: false,
296
+ deviceModel: String(EDeviceType.Classic1s),
297
+ })
298
+ ).not.toThrow();
299
+ } finally {
300
+ expect(unregisterFirmwareUpdateHostBinding(secondGeneration)).toBe(true);
301
+ }
302
+ });
303
+
304
+ test('a stale unregister cannot release the active workflow identity pin', () => {
305
+ const stalePlan = prepareRecoveryPlan();
306
+ const staleGeneration = registerFirmwareUpdateHostBinding(
307
+ createBinding(stalePlan.preparedPlanDigest)
308
+ );
309
+ const activePlan = prepareRecoveryPlan();
310
+ const activeGeneration = registerFirmwareUpdateHostBinding(
311
+ createBinding(activePlan.preparedPlanDigest)
312
+ );
313
+ try {
314
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
315
+ preparedPlan: activePlan,
316
+ deviceIdentity: 'CLA45F0023',
317
+ bootloaderMode: false,
318
+ deviceModel: String(EDeviceType.Classic1s),
319
+ });
320
+ expect(unregisterFirmwareUpdateHostBinding(staleGeneration)).toBe(false);
321
+ expect(() =>
322
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
323
+ preparedPlan: activePlan,
324
+ deviceIdentity: 'CLA99Z9999',
325
+ bootloaderMode: false,
326
+ deviceModel: String(EDeviceType.Classic1s),
327
+ })
328
+ ).toThrow();
329
+ } finally {
330
+ expect(unregisterFirmwareUpdateHostBinding(activeGeneration)).toBe(true);
331
+ }
332
+ });
253
333
  });
254
334
 
255
335
  describe('prepared plan device identity assertion', () => {
@@ -1,6 +1,9 @@
1
1
  import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
3
+ import {
4
+ buildFirmwareUpdatePlan,
5
+ validateProtocolV2FirmwareUpdateTargets,
6
+ } from '../../src/api/firmware/FirmwareUpdatePlan';
4
7
  import {
5
8
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
6
9
  prepareFirmwareUpdatePlan,
@@ -88,6 +91,18 @@ const createProtocolV2ForceInput = (
88
91
  forceUpdateTargets: forceUpdateTargets as BuildPlanInput['forceUpdateTargets'],
89
92
  });
90
93
 
94
+ describe('validateProtocolV2FirmwareUpdateTargets', () => {
95
+ test('normalizes the deprecated boot_resources alias to resource', () => {
96
+ expect(validateProtocolV2FirmwareUpdateTargets(['boot_resources'])).toEqual(['resource']);
97
+ });
98
+
99
+ test('rejects duplicate semantic targets after alias normalization', () => {
100
+ expectFirmwarePlanInvalid(() =>
101
+ validateProtocolV2FirmwareUpdateTargets(['boot_resources', 'resource'])
102
+ );
103
+ });
104
+ });
105
+
91
106
  describe('buildFirmwareUpdatePlan', () => {
92
107
  test('selects all legacy artifacts before execution and chooses the desktop full resource', () => {
93
108
  const plan = buildFirmwareUpdatePlan({
@@ -316,6 +331,51 @@ describe('buildFirmwareUpdatePlan', () => {
316
331
  expectFirmwarePlanInvalid(() => buildFirmwareUpdatePlan(createLegacyForceInput(['resource'])));
317
332
  });
318
333
 
334
+ test.each([
335
+ {
336
+ executor: 'V2',
337
+ deviceType: EDeviceType.Classic1s,
338
+ bootloaderVersion: '2.1.2',
339
+ },
340
+ {
341
+ executor: 'V3',
342
+ deviceType: EDeviceType.Pro,
343
+ bootloaderVersion: '2.8.4',
344
+ },
345
+ ])(
346
+ 'rejects a resource-only prepared plan that the $executor executor cannot run',
347
+ ({ deviceType, bootloaderVersion }) => {
348
+ expectFirmwarePlanInvalid(
349
+ () =>
350
+ buildFirmwareUpdatePlan({
351
+ features: createFeatures({
352
+ deviceType,
353
+ firmwareVersion: '4.21.0',
354
+ bootloaderVersion,
355
+ }),
356
+ firmwareType: EFirmwareType.Universal,
357
+ platform: 'desktop',
358
+ firmware: {
359
+ status: 'valid',
360
+ release: {
361
+ url: 'https://firmware.onekey.so/legacy/firmware.bin',
362
+ version: [4, 21, 0],
363
+ expectedSize: 1024,
364
+ fingerprint: '1'.repeat(64),
365
+ resource: 'https://firmware.onekey.so/legacy/resource.zip',
366
+ resourceExpectedSize: 4096,
367
+ resourceFingerprint: '2'.repeat(64),
368
+ },
369
+ },
370
+ ble: noUpdate,
371
+ bootloader: noUpdate,
372
+ forceUpdateTargets: ['resource'],
373
+ }),
374
+ 'Legacy resource updates require a firmware target'
375
+ );
376
+ }
377
+ );
378
+
319
379
  test('rejects a legacy resource archive without complete integrity metadata', () => {
320
380
  expectFirmwarePlanInvalid(
321
381
  () =>
@@ -57,6 +57,22 @@ describe('Protocol V2 firmware target contract', () => {
57
57
  ).toThrow('Neo only supports SE01 and SE02; unsupported firmware targets: se03, se04');
58
58
  });
59
59
 
60
+ test('ignores unselected SE binaries when the caller supplied a target subset', () => {
61
+ const params = {
62
+ platform: 'web' as const,
63
+ targetsToUpdate: ['app_v1'] as Array<'app_v1'>,
64
+ se03Binary: new ArrayBuffer(1),
65
+ se04Binary: new ArrayBuffer(1),
66
+ };
67
+
68
+ expect(() =>
69
+ assertProtocolV2FirmwareTargetsSupported(EDeviceType.Neo, params, true)
70
+ ).not.toThrow();
71
+ expect(() => assertProtocolV2FirmwareTargetsSupported(EDeviceType.Neo, params)).toThrow(
72
+ 'Neo only supports SE01 and SE02; unsupported firmware targets: se03, se04'
73
+ );
74
+ });
75
+
60
76
  test('keeps all four SE firmware targets available on Pro2', () => {
61
77
  expect(() =>
62
78
  assertProtocolV2FirmwareTargetsSupported(EDeviceType.Pro2, {
@@ -4033,6 +4033,71 @@ describe('Protocol V2 firmware update targets', () => {
4033
4033
  });
4034
4034
  });
4035
4035
 
4036
+ test('filters unrequested binaries from a direct local Protocol V2 update', async () => {
4037
+ const applicationP1Binary = new Uint8Array([1, 2, 3]).buffer;
4038
+ const method = new FirmwareUpdateV4({
4039
+ id: 1,
4040
+ payload: {
4041
+ method: 'firmwareUpdateV4',
4042
+ platform: 'web',
4043
+ targetsToUpdate: ['app_v1'],
4044
+ applicationP1Binary,
4045
+ bootloaderBinary: new Uint8Array([4, 5, 6]).buffer,
4046
+ },
4047
+ });
4048
+ method.init();
4049
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4050
+ (method as any).device = stubDevice({
4051
+ originalDescriptor: { protocolType: 'V2' },
4052
+ features: {
4053
+ deviceType: 'pro2',
4054
+ firmwareVersion: '1.0.0',
4055
+ capabilities: [],
4056
+ },
4057
+ getCurrentDeviceType: () => 'pro2',
4058
+ });
4059
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue('local-result');
4060
+ method.postTipMessage = jest.fn();
4061
+
4062
+ await expect(method.run()).resolves.toBe('local-result');
4063
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
4064
+ bootloaderBinary: null,
4065
+ fwBinaryMap: [
4066
+ {
4067
+ fileName: 'application_p1.bin',
4068
+ binary: applicationP1Binary,
4069
+ targetId: 4,
4070
+ },
4071
+ ],
4072
+ });
4073
+ });
4074
+
4075
+ test('keeps all local binaries when no target list was supplied', () => {
4076
+ const method = new FirmwareUpdateV4({
4077
+ id: 1,
4078
+ payload: {
4079
+ method: 'firmwareUpdateV4',
4080
+ platform: 'web',
4081
+ applicationP1Binary: new Uint8Array([1]).buffer,
4082
+ bootloaderBinary: new Uint8Array([2]).buffer,
4083
+ resourceArchiveBinary: new Uint8Array([3]).buffer,
4084
+ },
4085
+ });
4086
+ method.init();
4087
+
4088
+ const availableInstallItems = (method as any).buildProtocolV2InstallItems({
4089
+ bootloaderBinary: (method as any).prepareBootloaderBinary(),
4090
+ fwBinaryMap: (method as any).collectExplicitTargetBinaries(),
4091
+ });
4092
+
4093
+ expect((method as any).params.targetsToUpdate).toEqual(['resource']);
4094
+ expect(
4095
+ (method as any)
4096
+ .filterProtocolV2LocalInstallItems(availableInstallItems)
4097
+ .map((item: { targetId: number }) => item.targetId)
4098
+ ).toEqual([3, 4]);
4099
+ });
4100
+
4036
4101
  test('requires the external host to prepare manifest resources before bootloader entry', async () => {
4037
4102
  const method = new FirmwareUpdateV4({
4038
4103
  id: 1,
@@ -6702,6 +6767,7 @@ describe('Protocol V2 firmware update targets', () => {
6702
6767
  zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
6703
6768
  const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
6704
6769
  const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
6770
+ const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
6705
6771
  const method = new FirmwareUpdateV4({
6706
6772
  id: 1,
6707
6773
  payload: {
@@ -6709,6 +6775,7 @@ describe('Protocol V2 firmware update targets', () => {
6709
6775
  platform: 'web',
6710
6776
  targetsToUpdate: ['app_v1'],
6711
6777
  applicationP1Binary,
6778
+ bootloaderBinary,
6712
6779
  resourceArchiveBinary,
6713
6780
  },
6714
6781
  });
@@ -6742,6 +6809,11 @@ describe('Protocol V2 firmware update targets', () => {
6742
6809
  executor: 'v4',
6743
6810
  targetsToUpdate: ['app_v1', 'resource'],
6744
6811
  });
6812
+ expect(
6813
+ (method as any).params.preparedPlan.artifacts.some(
6814
+ (artifact: { target: string }) => artifact.target === 'boot'
6815
+ )
6816
+ ).toBe(false);
6745
6817
  expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
6746
6818
  expect.objectContaining({
6747
6819
  installSources: [
@@ -6832,6 +6904,30 @@ describe('Protocol V2 firmware update targets', () => {
6832
6904
  );
6833
6905
  });
6834
6906
 
6907
+ test('maps malformed resource ZIPs to a typed firmware preparation error', async () => {
6908
+ const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockRejectedValue(new Error('corrupt ZIP'));
6909
+ const method = new FirmwareUpdateV4({
6910
+ id: 1,
6911
+ payload: {
6912
+ method: 'firmwareUpdateV4',
6913
+ platform: 'web',
6914
+ targetsToUpdate: ['resource'],
6915
+ },
6916
+ });
6917
+ method.init();
6918
+
6919
+ try {
6920
+ await expect(
6921
+ (method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
6922
+ ).rejects.toMatchObject({
6923
+ errorCode: HardwareErrorCode.RuntimeError,
6924
+ params: { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' },
6925
+ });
6926
+ } finally {
6927
+ loadSpy.mockRestore();
6928
+ }
6929
+ });
6930
+
6835
6931
  test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
6836
6932
  const extractEntry = jest.fn();
6837
6933
  const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
@@ -1 +1 @@
1
- {"version":3,"file":"CheckAllFirmwareRelease.d.ts","sourceRoot":"","sources":["../../src/api/CheckAllFirmwareRelease.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAgC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EACV,kBAAkB,EAElB,kCAAkC,EAClC,+BAA+B,EAChC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EAGrB,MAAM,UAAU,CAAC;AAgIlB,MAAM,MAAM,6BAA6B,GAAG;IAC1C,YAAY,EAAE,aAAa,CAAC;IAC5B,MAAM,EAAE,+BAA+B,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,mBAAmB,CAAC;IACrC,UAAU,EAAE,kCAAkC,EAAE,CAAC;IACjD,eAAe,EAAE,sBAAsB,EAAE,CAAC;IAC1C,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEF,wBAAgB,8BAA8B,CAAC,EAC7C,eAAe,EACf,mBAAwB,EACxB,iBAAyB,EACzB,mBAAwB,EACxB,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,eAAe,EAAE,mBAAmB,CAAC;IACrC,mBAAmB,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,YAAY,EAAE,aAAa,CAAC;IAC5B,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC7B,GAAG,6BAA6B,CAiFhC;AAID,MAAM,CAAC,OAAO,OAAO,uBAAwB,SAAQ,UAAU;IAC7D,qBAAqB;IAIrB,IAAI;IAME,GAAG;YAuFK,aAAa;CAsH5B"}
1
+ {"version":3,"file":"CheckAllFirmwareRelease.d.ts","sourceRoot":"","sources":["../../src/api/CheckAllFirmwareRelease.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAgC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EACV,kBAAkB,EAElB,kCAAkC,EAClC,+BAA+B,EAChC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EAGrB,MAAM,UAAU,CAAC;AAgIlB,MAAM,MAAM,6BAA6B,GAAG;IAC1C,YAAY,EAAE,aAAa,CAAC;IAC5B,MAAM,EAAE,+BAA+B,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,mBAAmB,CAAC;IACrC,UAAU,EAAE,kCAAkC,EAAE,CAAC;IACjD,eAAe,EAAE,sBAAsB,EAAE,CAAC;IAC1C,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEF,wBAAgB,8BAA8B,CAAC,EAC7C,eAAe,EACf,mBAAwB,EACxB,iBAAyB,EACzB,mBAAwB,EACxB,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,eAAe,EAAE,mBAAmB,CAAC;IACrC,mBAAmB,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,YAAY,EAAE,aAAa,CAAC;IAC5B,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC7B,GAAG,6BAA6B,CAiFhC;AAID,MAAM,CAAC,OAAO,OAAO,uBAAwB,SAAQ,UAAU;IAC7D,qBAAqB;IAIrB,IAAI;IAME,GAAG;YAuFK,aAAa;CA2H5B"}