@onekeyfe/hd-core 1.2.0-alpha.84 → 1.2.0-alpha.86

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 (58) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +69 -26
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +1 -2
  3. package/__tests__/firmware-memory-host.test.ts +108 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +4 -1
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-update-plan.test.ts +15 -70
  7. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  8. package/__tests__/protocol-v2-resources.test.ts +59 -39
  9. package/__tests__/protocol-v2.test.ts +255 -306
  10. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV4.d.ts +1 -2
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  14. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareMemoryHost.d.ts +27 -0
  16. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  17. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +3 -4
  18. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  19. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  20. package/dist/api/firmware/getBinary.d.ts +0 -1
  21. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  22. package/dist/data-manager/DataManager.d.ts +3 -1
  23. package/dist/data-manager/DataManager.d.ts.map +1 -1
  24. package/dist/index.d.ts +32 -70
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +450 -390
  27. package/dist/inject.d.ts.map +1 -1
  28. package/dist/protocols/protocol-v2/resources.d.ts +0 -17
  29. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  30. package/dist/types/api/checkAllFirmwareRelease.d.ts +0 -1
  31. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  32. package/dist/types/api/export.d.ts +0 -1
  33. package/dist/types/api/export.d.ts.map +1 -1
  34. package/dist/types/api/firmwareUpdate.d.ts +1 -10
  35. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  36. package/dist/types/api/index.d.ts +0 -2
  37. package/dist/types/api/index.d.ts.map +1 -1
  38. package/dist/types/settings.d.ts +3 -15
  39. package/dist/types/settings.d.ts.map +1 -1
  40. package/package.json +4 -4
  41. package/src/api/CheckAllFirmwareRelease.ts +4 -13
  42. package/src/api/FirmwareUpdateV4.ts +166 -170
  43. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  44. package/src/api/firmware/FirmwareMemoryHost.ts +174 -0
  45. package/src/api/firmware/FirmwareUpdatePlan.ts +28 -48
  46. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +8 -1
  47. package/src/data-manager/DataManager.ts +33 -13
  48. package/src/index.ts +6 -1
  49. package/src/inject.ts +0 -2
  50. package/src/protocols/protocol-v2/resources.ts +0 -47
  51. package/src/types/api/checkAllFirmwareRelease.ts +0 -1
  52. package/src/types/api/export.ts +0 -4
  53. package/src/types/api/firmwareUpdate.ts +2 -15
  54. package/src/types/api/index.ts +0 -2
  55. package/src/types/settings.ts +3 -25
  56. package/dist/types/api/protocolV2ResourceManifest.d.ts +0 -17
  57. package/dist/types/api/protocolV2ResourceManifest.d.ts.map +0 -1
  58. package/src/types/api/protocolV2ResourceManifest.ts +0 -19
@@ -62,14 +62,6 @@ const release: IFirmwareReleaseInfo = {
62
62
  version: [2, 0, 0],
63
63
  },
64
64
  },
65
- resourceBundles: [
66
- {
67
- name: 'images',
68
- url: 'https://example.com/images.okpkg',
69
- devicePath: 'vol0:/bundles/images/images.okpkg',
70
- version: [1, 0, 0],
71
- },
72
- ],
73
65
  };
74
66
 
75
67
  const resourceSource = {
@@ -152,7 +144,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
152
144
  test('detects same-version package hotfixes by P1 and P2 payload hash', () => {
153
145
  const packageSet: IFirmwareReleaseInfo = {
154
146
  ...release,
155
- resourceBundles: undefined,
156
147
  installOrder: ['applicationP1', 'applicationP2'],
157
148
  components: {
158
149
  applicationP1: {
@@ -203,7 +194,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
203
194
  const packageSet: IFirmwareReleaseInfo = {
204
195
  ...release,
205
196
  required: false,
206
- resourceBundles: undefined,
207
197
  installOrder: ['applicationP1'],
208
198
  components: {
209
199
  applicationP1: {
@@ -234,7 +224,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
234
224
  const packageSet: IFirmwareReleaseInfo = {
235
225
  ...release,
236
226
  required: false,
237
- resourceBundles: undefined,
238
227
  installOrder: ['applicationP1'],
239
228
  components: {
240
229
  applicationP1: {
@@ -265,7 +254,6 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
265
254
  test('updates both application packages when normal mode only reports P1', () => {
266
255
  const packageSet: IFirmwareReleaseInfo = {
267
256
  ...release,
268
- resourceBundles: undefined,
269
257
  installOrder: ['applicationP1', 'applicationP2'],
270
258
  components: {
271
259
  applicationP1: {
@@ -331,8 +319,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
331
319
  status: 'required',
332
320
  resourceStatus: 'unknown',
333
321
  resourceArchive: resourceSource,
334
- resourcePreparationRequired: true,
335
- targetsToUpdate: ['boot', 'app_v1', 'resource'],
322
+ targetsToUpdate: ['boot', 'app_v1'],
336
323
  firmwareUpdatePlan: {
337
324
  executor: 'v4',
338
325
  platform: 'web',
@@ -381,7 +368,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
381
368
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
382
369
 
383
370
  await expect(method.run()).resolves.toMatchObject({
384
- targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03', 'resource'],
371
+ targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03'],
385
372
  firmwareUpdatePlan: {
386
373
  executor: 'v4',
387
374
  deviceIdentity: 'pro2-device-id',
@@ -442,7 +429,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
442
429
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
443
430
 
444
431
  await expect(method.run()).resolves.toMatchObject({
445
- targetsToUpdate: ['app_v1', 'coprocessor', 'resource'],
432
+ targetsToUpdate: ['app_v1', 'coprocessor'],
446
433
  firmwareUpdatePlan: {
447
434
  executor: 'v4',
448
435
  platform: 'desktop',
@@ -491,7 +478,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
491
478
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
492
479
 
493
480
  await expect(method.run()).resolves.toMatchObject({
494
- targetsToUpdate: ['app_v1', 'resource'],
481
+ targetsToUpdate: ['app_v1'],
495
482
  firmwareUpdatePlan: undefined,
496
483
  });
497
484
  });
@@ -571,7 +558,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
571
558
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
572
559
 
573
560
  await expect(method.run()).resolves.toMatchObject({
574
- targetsToUpdate: ['se01', 'se02', 'resource'],
561
+ targetsToUpdate: ['se01', 'se02'],
575
562
  firmwareUpdatePlan: {
576
563
  executor: 'v4',
577
564
  deviceModel: 'neo',
@@ -607,11 +594,68 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
607
594
  });
608
595
  });
609
596
 
597
+ test('adds a Protocol V2 resource target only when explicitly forced', async () => {
598
+ const currentRelease: IFirmwareReleaseInfo = {
599
+ ...release,
600
+ required: false,
601
+ installOrder: ['applicationP1'],
602
+ components: {
603
+ applicationP1: {
604
+ target: 'APPLICATION_P1',
605
+ url: 'https://example.com/application-p1.okpkg',
606
+ version: [1, 0, 0],
607
+ },
608
+ },
609
+ };
610
+ const method = new CheckAllFirmwareRelease({
611
+ id: 1,
612
+ payload: {
613
+ method: 'checkAllFirmwareRelease',
614
+ platform: 'desktop',
615
+ protocolV2ForceUpdateTargets: ['resource'],
616
+ },
617
+ });
618
+ method.init();
619
+ method.device = {
620
+ isProtocolV2: () => true,
621
+ features: {
622
+ deviceType: 'pro2',
623
+ serialNo: 'pro2-device-id',
624
+ firmwareVersion: '1.0.0',
625
+ },
626
+ getDeviceState: jest.fn().mockResolvedValue({
627
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
628
+ status: { mode: 'normal' },
629
+ versions: { ...currentVersions, applicationP1: '1.0.0' },
630
+ }),
631
+ } as unknown as CheckAllFirmwareRelease['device'];
632
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(currentRelease);
633
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
634
+
635
+ await expect(method.run()).resolves.toMatchObject({
636
+ status: 'valid',
637
+ hasUpgrade: true,
638
+ targetsToUpdate: ['resource'],
639
+ firmwareUpdatePlan: {
640
+ executor: 'v4',
641
+ platform: 'desktop',
642
+ artifacts: [
643
+ {
644
+ artifactId: 'resource:archive',
645
+ target: 'resource',
646
+ role: 'resourceBundle',
647
+ container: 'zip',
648
+ },
649
+ ],
650
+ targetsToUpdate: ['resource'],
651
+ },
652
+ });
653
+ });
654
+
610
655
  test('requests and compares firmware hashes only when explicitly enabled', async () => {
611
656
  const packageSet: IFirmwareReleaseInfo = {
612
657
  ...release,
613
658
  required: false,
614
- resourceBundles: undefined,
615
659
  installOrder: ['applicationP1'],
616
660
  components: {
617
661
  applicationP1: {
@@ -684,14 +728,13 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
684
728
  protocol: 'V2',
685
729
  resourceStatus: 'unknown',
686
730
  resourceArchive: resourceSource,
687
- resourcePreparationRequired: true,
688
- targetsToUpdate: ['boot', 'app_v1', 'resource'],
731
+ targetsToUpdate: ['boot', 'app_v1'],
689
732
  });
690
733
  expect(typedCall).not.toHaveBeenCalled();
691
734
  }
692
735
  );
693
736
 
694
- test('requires manifest preparation when only Protocol V2 resources may have changed', async () => {
737
+ test('exposes resource archive availability without reporting an unconfirmed update', async () => {
695
738
  const currentRelease: IFirmwareReleaseInfo = {
696
739
  ...release,
697
740
  required: false,
@@ -725,11 +768,11 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
725
768
  jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
726
769
 
727
770
  await expect(method.run()).resolves.toMatchObject({
728
- status: 'unknown',
729
- hasUpgrade: true,
771
+ status: 'valid',
772
+ hasUpgrade: false,
730
773
  resourceStatus: 'unknown',
731
- resourcePreparationRequired: true,
732
- targetsToUpdate: ['resource'],
774
+ resourceArchive: resourceSource,
775
+ targetsToUpdate: [],
733
776
  firmwareUpdatePlan: undefined,
734
777
  });
735
778
  });
@@ -147,8 +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
- resourcePreparationRequired: true,
151
- targetsToUpdate: ['boot', 'app_v1', 'coprocessor', 'resource'],
150
+ targetsToUpdate: ['boot', 'app_v1', 'coprocessor'],
152
151
  firmware: { status: 'outdated', shouldUpdate: true },
153
152
  ble: {
154
153
  status: 'outdated',
@@ -0,0 +1,108 @@
1
+ import { prepareFirmwareUpdateV4MemoryHost } from '../src/api/firmware/FirmwareMemoryHost';
2
+
3
+ import type { CoreApi } from '../src/types/api';
4
+ import type { FirmwareUpdatePlan } from '../src/types/api/firmwareUpdatePlan';
5
+
6
+ describe('prepareFirmwareUpdateV4MemoryHost', () => {
7
+ test('registers artifact-backed component and resource readers', async () => {
8
+ const componentBinary = new Uint8Array([1, 2, 3, 4]).buffer;
9
+ const archiveBinary = new Uint8Array([5, 6]).buffer;
10
+ const manifestBinary = new TextEncoder().encode('{"schema":1}').buffer;
11
+ let artifactReader: Parameters<
12
+ CoreApi['registerFirmwareUpdateHostBinding']
13
+ >[0]['artifactReader'];
14
+ const unregisterFirmwareUpdateHostBinding = jest.fn();
15
+ const sdk = {
16
+ prepareFirmwareUpdatePlan: jest.fn(({ plan, artifacts }) => ({
17
+ schemaVersion: 2,
18
+ preparedPlanDigest: 'b'.repeat(64),
19
+ planDigest: plan.planDigest,
20
+ networkPolicy: 'forbid',
21
+ executor: plan.executor,
22
+ deviceIdentity: plan.deviceIdentity,
23
+ deviceModel: plan.deviceModel,
24
+ firmwareType: plan.firmwareType,
25
+ platform: plan.platform,
26
+ leaseRef: 'test-lease',
27
+ targetsToUpdate: plan.targetsToUpdate,
28
+ artifacts: plan.artifacts.map(planArtifact => {
29
+ const input = artifacts.find(item => item.artifactId === planArtifact.artifactId);
30
+ return {
31
+ ...planArtifact,
32
+ artifact: input?.artifact,
33
+ materializedEntries: input?.materializedEntries,
34
+ };
35
+ }),
36
+ })),
37
+ registerFirmwareUpdateHostBinding: jest.fn(binding => {
38
+ artifactReader = binding.artifactReader;
39
+ return 7;
40
+ }),
41
+ unregisterFirmwareUpdateHostBinding,
42
+ } as unknown as Pick<
43
+ CoreApi,
44
+ | 'prepareFirmwareUpdatePlan'
45
+ | 'registerFirmwareUpdateHostBinding'
46
+ | 'unregisterFirmwareUpdateHostBinding'
47
+ >;
48
+ const plan = {
49
+ schemaVersion: 2,
50
+ planDigest: 'a'.repeat(64),
51
+ executor: 'v4',
52
+ deviceIdentity: 'device-id',
53
+ deviceModel: 'pro2',
54
+ firmwareType: 0,
55
+ platform: 'web',
56
+ targetsToUpdate: ['boot', 'resource'],
57
+ artifacts: [
58
+ {
59
+ artifactId: 'component:boot',
60
+ role: 'component',
61
+ target: 'boot',
62
+ url: 'https://example.com/boot.okpkg',
63
+ container: 'raw',
64
+ },
65
+ {
66
+ artifactId: 'resource:archive',
67
+ role: 'resourceBundle',
68
+ target: 'resource',
69
+ url: 'https://example.com/resource.zip',
70
+ container: 'zip',
71
+ logicalName: 'resource-archive',
72
+ },
73
+ ],
74
+ } as FirmwareUpdatePlan;
75
+
76
+ const host = prepareFirmwareUpdateV4MemoryHost({
77
+ sdk,
78
+ plan,
79
+ artifacts: [
80
+ { artifactId: 'component:boot', binary: componentBinary },
81
+ {
82
+ artifactId: 'resource:archive',
83
+ binary: archiveBinary,
84
+ materializedEntries: [{ entryName: 'manifest.json', binary: manifestBinary }],
85
+ },
86
+ ],
87
+ });
88
+
89
+ expect(host.hostBindingGeneration).toBe(7);
90
+ expect(sdk.registerFirmwareUpdateHostBinding).toHaveBeenCalledWith(
91
+ expect.objectContaining({ preparedPlanDigest: host.preparedPlan.preparedPlanDigest })
92
+ );
93
+ expect(host.componentArtifacts.boot?.size).toBe(componentBinary.byteLength);
94
+ const opened = await artifactReader!.open({
95
+ artifactRef: host.componentArtifacts.boot!.artifactRef,
96
+ });
97
+ const chunk = await artifactReader!.read({
98
+ readerId: opened.readerId,
99
+ offset: 1,
100
+ length: 2,
101
+ });
102
+ expect(Array.from(new Uint8Array(chunk.data))).toEqual([2, 3]);
103
+ await artifactReader!.close({ readerId: opened.readerId });
104
+
105
+ host.release();
106
+ expect(unregisterFirmwareUpdateHostBinding).toHaveBeenCalledWith(7);
107
+ });
108
+ });
@@ -36,7 +36,10 @@ const createMethod = () => {
36
36
  read: jest.fn(),
37
37
  close,
38
38
  };
39
- const hostBindingGeneration = registerFirmwareUpdateHostBinding({ artifactReader });
39
+ const hostBindingGeneration = registerFirmwareUpdateHostBinding({
40
+ artifactReader,
41
+ preparedPlanDigest: 'a'.repeat(64),
42
+ });
40
43
  const method = new DeviceUpdateBootloader({
41
44
  id: 1,
42
45
  payload: {
@@ -6,6 +6,7 @@ import {
6
6
  } from '../../src/api/firmware/FirmwareHostBinding';
7
7
 
8
8
  const createBinding = () => ({
9
+ preparedPlanDigest: 'a'.repeat(64),
9
10
  artifactReader: {
10
11
  open: jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: 4 })),
11
12
  read: jest.fn(() =>
@@ -67,4 +68,12 @@ describe('firmware host binding generation', () => {
67
68
  readerId: 'stale-reader',
68
69
  });
69
70
  });
71
+
72
+ test('rejects a prepared plan from another host generation before opening artifacts', () => {
73
+ const generation = registerFirmwareUpdateHostBinding(createBinding());
74
+
75
+ expect(() => resolveFirmwareUpdateHostBinding(generation, 'b'.repeat(64))).toThrow(
76
+ 'does not match the prepared plan'
77
+ );
78
+ });
70
79
  });
@@ -297,12 +297,6 @@ describe('buildFirmwareUpdatePlan', () => {
297
297
  forceTarget: 'firmware' as const,
298
298
  release: {
299
299
  components: {},
300
- resourceBundles: [
301
- {
302
- name: 'images',
303
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
304
- },
305
- ],
306
300
  },
307
301
  },
308
302
  {
@@ -325,41 +319,21 @@ describe('buildFirmwareUpdatePlan', () => {
325
319
  }
326
320
  );
327
321
 
328
- test.each([
329
- {
330
- forceTarget: 'firmware' as const,
331
- expectedArtifactIds: ['component:app_v1'],
332
- expectedTargets: ['app_v1'],
333
- },
334
- {
335
- forceTarget: 'resource' as const,
336
- expectedArtifactIds: ['resourceBundle:images'],
337
- expectedTargets: ['resource'],
338
- },
339
- ])(
340
- 'limits a Pro2 $forceTarget force to the selected artifact role',
341
- ({ forceTarget, expectedArtifactIds, expectedTargets }) => {
342
- const plan = buildFirmwareUpdatePlan(
343
- createProtocolV2ForceInput([forceTarget], {
344
- components: {
345
- applicationP1: {
346
- target: 'APPLICATION_P1',
347
- url: 'https://firmware.onekey.so/pro2/application-p1.bin',
348
- },
322
+ test('limits a Pro2 firmware force to component artifacts', () => {
323
+ const plan = buildFirmwareUpdatePlan(
324
+ createProtocolV2ForceInput(['firmware'], {
325
+ components: {
326
+ applicationP1: {
327
+ target: 'APPLICATION_P1',
328
+ url: 'https://firmware.onekey.so/pro2/application-p1.bin',
349
329
  },
350
- resourceBundles: [
351
- {
352
- name: 'images',
353
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
354
- },
355
- ],
356
- })
357
- );
330
+ },
331
+ })
332
+ );
358
333
 
359
- expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(expectedArtifactIds);
360
- expect(plan.targetsToUpdate).toEqual(expectedTargets);
361
- }
362
- );
334
+ expect(plan.artifacts.map(artifact => artifact.artifactId)).toEqual(['component:app_v1']);
335
+ expect(plan.targetsToUpdate).toEqual(['app_v1']);
336
+ });
363
337
 
364
338
  test('rejects a prepared native plan without a stable device identity', () => {
365
339
  const features = createFeatures({
@@ -384,7 +358,7 @@ describe('buildFirmwareUpdatePlan', () => {
384
358
  ).toThrow('requires a stable device identity');
385
359
  });
386
360
 
387
- test('maps the Pro2 component order and resource bundles without downloading them', () => {
361
+ test('maps the Pro2 component order without downloading artifacts', () => {
388
362
  const plan = buildFirmwareUpdatePlan({
389
363
  features: createFeatures({ deviceType: EDeviceType.Pro2 }),
390
364
  firmwareType: EFirmwareType.Universal,
@@ -409,12 +383,6 @@ describe('buildFirmwareUpdatePlan', () => {
409
383
  url: 'https://firmware.onekey.so/pro2/se01.bin',
410
384
  },
411
385
  },
412
- resourceBundles: [
413
- {
414
- name: 'images',
415
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
416
- },
417
- ],
418
386
  },
419
387
  },
420
388
  ble: noUpdate,
@@ -426,9 +394,8 @@ describe('buildFirmwareUpdatePlan', () => {
426
394
  'component:boot',
427
395
  'component:app_v1',
428
396
  'component:se01',
429
- 'resourceBundle:images',
430
397
  ]);
431
- expect(plan.targetsToUpdate).toEqual(['boot', 'app_v1', 'se01', 'resource']);
398
+ expect(plan.targetsToUpdate).toEqual(['boot', 'app_v1', 'se01']);
432
399
  expect(plan.artifacts[1]).toEqual(
433
400
  expect.objectContaining({
434
401
  expectedSize: 1024,
@@ -477,28 +444,6 @@ describe('buildFirmwareUpdatePlan', () => {
477
444
  },
478
445
  expectedError: 'duplicates target app_v1',
479
446
  },
480
- {
481
- label: 'resource bundle name',
482
- release: {
483
- components: {
484
- application: {
485
- target: 'APPLICATION_P1',
486
- url: 'https://firmware.onekey.so/pro2/application.bin',
487
- },
488
- },
489
- resourceBundles: [
490
- {
491
- name: 'images',
492
- url: 'https://firmware.onekey.so/pro2/images.okpkg',
493
- },
494
- {
495
- name: 'images',
496
- url: 'https://firmware.onekey.so/pro2/images-duplicate.okpkg',
497
- },
498
- ],
499
- },
500
- expectedError: 'duplicates name images',
501
- },
502
447
  ])('rejects a duplicate Pro2 $label', ({ release, expectedError }) => {
503
448
  expect(() =>
504
449
  buildFirmwareUpdatePlan({
@@ -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
+ digestFirmwareUpdateContract,
6
+ } from '../../src/api/firmware/FirmwareUpdatePlan';
4
7
  import {
5
8
  assertFirmwareUpdatePreparedPlanBinding,
6
9
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
@@ -209,6 +212,55 @@ describe('FirmwareUpdatePreparedPlan', () => {
209
212
  ).toThrow('entry name is invalid');
210
213
  });
211
214
 
215
+ test('rejects duplicate materialized entry names during standalone validation', () => {
216
+ const plan = buildPlan();
217
+ const prepared = prepareFirmwareUpdatePlan({
218
+ plan,
219
+ leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
220
+ artifacts: [
221
+ {
222
+ artifactId: 'firmware',
223
+ artifact: artifact('a'.repeat(64), 4),
224
+ },
225
+ {
226
+ artifactId: 'resource',
227
+ artifact: artifact('b'.repeat(64), 8),
228
+ materializedEntries: [
229
+ {
230
+ entryName: 'resource/index.bin',
231
+ artifact: artifact('c'.repeat(64), 2),
232
+ },
233
+ ],
234
+ },
235
+ ],
236
+ });
237
+ const resourceArtifact = prepared.artifacts.find(item => item.target === 'resource')!;
238
+ const tamperedWithoutDigest = {
239
+ ...prepared,
240
+ artifacts: prepared.artifacts.map(item =>
241
+ item === resourceArtifact
242
+ ? {
243
+ ...item,
244
+ materializedEntries: [
245
+ ...resourceArtifact.materializedEntries!,
246
+ {
247
+ entryName: 'other/index.bin',
248
+ artifact: artifact('d'.repeat(64), 2),
249
+ },
250
+ ],
251
+ }
252
+ : item
253
+ ),
254
+ };
255
+ const { preparedPlanDigest: _preparedPlanDigest, ...digestInput } = tamperedWithoutDigest;
256
+ const tampered = {
257
+ ...tamperedWithoutDigest,
258
+ preparedPlanDigest: digestFirmwareUpdateContract(digestInput),
259
+ };
260
+
261
+ expect(() => validateFirmwareUpdatePreparedPlan(tampered)).toThrow('duplicate entry names');
262
+ });
263
+
212
264
  test('rejects unknown fields even when a caller presents a plan-shaped object', () => {
213
265
  const plan = buildPlan();
214
266
  expect(() =>
@@ -1,13 +1,11 @@
1
1
  import axios from 'axios';
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
4
4
 
5
5
  import { DataManager } from '../src/data-manager';
6
6
  import {
7
- isProtocolV2ResourceFileValid,
8
7
  parseProtocolV2ResourceManifest,
9
8
  parseProtocolV2Resources,
10
- prepareProtocolV2ResourceFiles,
11
9
  } from '../src/protocols/protocol-v2/resources';
12
10
 
13
11
  import type { ConnectSettings, RemoteConfigResponse } from '../src/types';
@@ -27,6 +25,12 @@ const resourceSource = {
27
25
  archiveSize: 16_815_479,
28
26
  };
29
27
 
28
+ const neoResourceSource = {
29
+ archiveUrl: 'https://example.com/resource/neo-resource.zip',
30
+ archiveSha256: 'b'.repeat(64),
31
+ archiveSize: 12_345_678,
32
+ };
33
+
30
34
  const manifestFiles = [
31
35
  ['bundles/firmware_logo-build.okpkg', 'firmware_logo.okpkg', 'vol0:/bundles/firmware_logo.okpkg'],
32
36
  ['bundles/font/noto-build.okpkg', 'noto.okpkg', 'vol0:/bundles/font/noto.okpkg'],
@@ -105,6 +109,7 @@ const createRemoteConfig = (): RemoteConfigResponse =>
105
109
  touch: { firmware: [], ble: [] },
106
110
  pro: { firmware: [], ble: [] },
107
111
  pro2: { firmware: [], ble: [], resources: { source: resourceSource } },
112
+ neo: { firmware: [], ble: [], resources: { source: neoResourceSource } },
108
113
  bridge: {},
109
114
  } as unknown as RemoteConfigResponse);
110
115
 
@@ -151,42 +156,6 @@ describe('Pro2 resource configuration', () => {
151
156
  ).toThrow('archive_path');
152
157
  });
153
158
 
154
- test('verifies selected manifest files and preserves manifest device paths', () => {
155
- const prepared = prepareProtocolV2ResourceFiles({
156
- manifest: resourceManifest,
157
- files: manifestFiles.map(file => ({
158
- archivePath: file.archive_path,
159
- binary: file.binary,
160
- })),
161
- targetsToUpdate: ['resource'],
162
- });
163
- expect(prepared.map(file => file.devicePath)).toEqual(
164
- manifestFiles.map(file => file.device_path)
165
- );
166
- expect(() =>
167
- prepareProtocolV2ResourceFiles({
168
- manifest: resourceManifest,
169
- files: manifestFiles.map((file, index) => ({
170
- archivePath: file.archive_path,
171
- binary: index === 0 ? new Uint8Array([0]).buffer : file.binary,
172
- })),
173
- targetsToUpdate: ['resource'],
174
- })
175
- ).toThrow('verification failed');
176
- });
177
-
178
- test('verifies both full file size and SHA-256 before transfer', () => {
179
- const bytes = new Uint8Array([1, 2, 3]);
180
- const binary = bytes.buffer;
181
- const identity = { size: bytes.byteLength, fileHash: bytesToHex(sha256(bytes)) };
182
-
183
- expect(isProtocolV2ResourceFileValid(binary, identity)).toBe(true);
184
- expect(isProtocolV2ResourceFileValid(binary, { ...identity, size: 4 })).toBe(false);
185
- expect(isProtocolV2ResourceFileValid(binary, { ...identity, fileHash: '0'.repeat(64) })).toBe(
186
- false
187
- );
188
- });
189
-
190
159
  test('applies a validated pre-release config and exposes its archive source', async () => {
191
160
  const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
192
161
 
@@ -217,6 +186,34 @@ describe('Pro2 resource configuration', () => {
217
186
  await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
218
187
 
219
188
  expect(DataManager.getProtocolV2ResourceSource()).toBeUndefined();
189
+ expect(DataManager.protocolV2ResourcesConfigError).toBeInstanceOf(Error);
190
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Neo)).toEqual(neoResourceSource);
191
+ });
192
+
193
+ test('keeps Pro2 resources available when remote Neo resources are invalid', async () => {
194
+ const remoteConfig = createRemoteConfig();
195
+ (remoteConfig.neo as { resources?: unknown }).resources = { source: {} };
196
+ const configFetcher = jest.fn().mockResolvedValue(remoteConfig);
197
+
198
+ await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
199
+
200
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Pro2)).toEqual(resourceSource);
201
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Neo)).toBeUndefined();
202
+ await expect(
203
+ DataManager.forceReloadData({
204
+ requireResources: true,
205
+ resourceDeviceType: EDeviceType.Pro2,
206
+ })
207
+ ).resolves.toBeUndefined();
208
+ await expect(
209
+ DataManager.forceReloadData({
210
+ requireResources: true,
211
+ resourceDeviceType: EDeviceType.Neo,
212
+ })
213
+ ).rejects.toMatchObject({
214
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
215
+ message: expect.stringContaining('Invalid Neo resources config'),
216
+ });
220
217
  });
221
218
 
222
219
  test('only blocks resource mutation when the refreshed Pro2 resources are invalid', async () => {
@@ -233,6 +230,29 @@ describe('Pro2 resource configuration', () => {
233
230
  expect(DataManager.lastCheckTimestamp).toBeGreaterThan(0);
234
231
  });
235
232
 
233
+ test('checks resource configuration errors for the requested device only', async () => {
234
+ const remoteConfig = createRemoteConfig();
235
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
236
+ const settings = createSettings(jest.fn().mockResolvedValue(remoteConfig));
237
+ DataManager.settings = settings;
238
+
239
+ await expect(
240
+ DataManager.forceReloadData({
241
+ requireResources: true,
242
+ resourceDeviceType: EDeviceType.Neo,
243
+ })
244
+ ).resolves.toBeUndefined();
245
+ await expect(
246
+ DataManager.forceReloadData({
247
+ requireResources: true,
248
+ resourceDeviceType: EDeviceType.Pro2,
249
+ })
250
+ ).rejects.toMatchObject({
251
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
252
+ message: expect.stringContaining('Invalid Pro2 resources config'),
253
+ });
254
+ });
255
+
236
256
  test('does not advance the cache timestamp when refresh fails', async () => {
237
257
  jest.spyOn(axios, 'get').mockRejectedValue(new Error('offline'));
238
258
  const settings = createSettings(jest.fn().mockResolvedValue(null));