@onekeyfe/hd-core 1.2.0-alpha.82 → 1.2.0-alpha.85

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 (53) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +285 -13
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +6 -0
  3. package/__tests__/firmware-memory-host.test.ts +105 -0
  4. package/__tests__/firmware-update/firmware-update-plan.test.ts +15 -70
  5. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  6. package/__tests__/protocol-v2-resources.test.ts +0 -38
  7. package/__tests__/protocol-v2.test.ts +212 -320
  8. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  9. package/dist/api/FirmwareUpdateV4.d.ts +1 -2
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/firmware/FirmwareMemoryHost.d.ts +27 -0
  12. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  13. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +33 -1
  14. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  16. package/dist/api/firmware/getBinary.d.ts +0 -1
  17. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  18. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  19. package/dist/index.d.ts +25 -64
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +546 -387
  22. package/dist/inject.d.ts.map +1 -1
  23. package/dist/protocols/protocol-v2/resources.d.ts +0 -17
  24. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  25. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -0
  26. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  27. package/dist/types/api/export.d.ts +0 -1
  28. package/dist/types/api/export.d.ts.map +1 -1
  29. package/dist/types/api/firmwareUpdate.d.ts +0 -10
  30. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  31. package/dist/types/api/index.d.ts +0 -2
  32. package/dist/types/api/index.d.ts.map +1 -1
  33. package/dist/types/settings.d.ts +0 -11
  34. package/dist/types/settings.d.ts.map +1 -1
  35. package/package.json +4 -4
  36. package/src/api/CheckAllFirmwareRelease.ts +72 -5
  37. package/src/api/FirmwareUpdateV4.ts +154 -165
  38. package/src/api/firmware/FirmwareMemoryHost.ts +171 -0
  39. package/src/api/firmware/FirmwareUpdatePlan.ts +176 -79
  40. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +8 -1
  41. package/src/api/firmware/protocolV2Release.ts +2 -0
  42. package/src/index.ts +6 -1
  43. package/src/inject.ts +0 -2
  44. package/src/protocols/protocol-v2/resources.ts +0 -47
  45. package/src/types/api/checkAllFirmwareRelease.ts +1 -0
  46. package/src/types/api/export.ts +0 -4
  47. package/src/types/api/firmwareUpdate.ts +0 -15
  48. package/src/types/api/index.ts +0 -2
  49. package/src/types/settings.ts +0 -20
  50. package/src/utils/deviceFeaturesUtils.ts +2 -2
  51. package/dist/types/api/protocolV2ResourceManifest.d.ts +0 -17
  52. package/dist/types/api/protocolV2ResourceManifest.d.ts.map +0 -1
  53. 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: {
@@ -333,6 +321,21 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
333
321
  resourceArchive: resourceSource,
334
322
  resourcePreparationRequired: true,
335
323
  targetsToUpdate: ['boot', 'app_v1', 'resource'],
324
+ firmwareUpdatePlan: {
325
+ executor: 'v4',
326
+ platform: 'web',
327
+ artifacts: [
328
+ { artifactId: 'component:boot', target: 'boot' },
329
+ { artifactId: 'component:app_v1', target: 'app_v1' },
330
+ {
331
+ artifactId: 'resource:archive',
332
+ target: 'resource',
333
+ role: 'resourceBundle',
334
+ container: 'zip',
335
+ },
336
+ ],
337
+ targetsToUpdate: ['boot', 'app_v1', 'resource'],
338
+ },
336
339
  firmware: {
337
340
  status: 'required',
338
341
  },
@@ -344,11 +347,266 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
344
347
  expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
345
348
  });
346
349
 
350
+ test('preserves platform and forced firmware targets in the Protocol V2 prepared plan', async () => {
351
+ const method = new CheckAllFirmwareRelease({
352
+ id: 1,
353
+ payload: {
354
+ method: 'checkAllFirmwareRelease',
355
+ firmwareType: EFirmwareType.Universal,
356
+ platform: 'native',
357
+ forceUpdateTargets: ['firmware'],
358
+ },
359
+ });
360
+ method.init();
361
+ method.device = {
362
+ isProtocolV2: () => true,
363
+ features: {
364
+ deviceType: 'pro2',
365
+ serialNo: 'pro2-device-id',
366
+ firmwareVersion: '1.0.0',
367
+ },
368
+ getDeviceState: jest.fn().mockResolvedValue({
369
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
370
+ status: { mode: 'normal' },
371
+ versions: currentVersions,
372
+ }),
373
+ } as unknown as CheckAllFirmwareRelease['device'];
374
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
375
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
376
+
377
+ await expect(method.run()).resolves.toMatchObject({
378
+ targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03', 'resource'],
379
+ firmwareUpdatePlan: {
380
+ executor: 'v4',
381
+ deviceIdentity: 'pro2-device-id',
382
+ platform: 'native',
383
+ artifacts: [
384
+ { artifactId: 'component:boot', target: 'boot' },
385
+ { artifactId: 'component:app_v1', target: 'app_v1' },
386
+ { artifactId: 'component:se02', target: 'se02' },
387
+ { artifactId: 'component:se03', target: 'se03' },
388
+ { artifactId: 'resource:archive', target: 'resource', container: 'zip' },
389
+ ],
390
+ targetsToUpdate: ['boot', 'app_v1', 'se02', 'se03', 'resource'],
391
+ },
392
+ });
393
+ });
394
+
395
+ test('includes exact Protocol V2 developer force targets in the prepared plan', async () => {
396
+ const currentRelease: IFirmwareReleaseInfo = {
397
+ ...release,
398
+ required: false,
399
+ version: [1, 0, 0],
400
+ installOrder: ['applicationP1', 'coprocessor'],
401
+ components: {
402
+ applicationP1: {
403
+ target: 'APPLICATION_P1',
404
+ url: 'https://example.com/application-p1.okpkg',
405
+ version: [1, 0, 0],
406
+ },
407
+ coprocessor: {
408
+ target: 'COPROCESSOR',
409
+ url: 'https://example.com/coprocessor.okpkg',
410
+ version: [1, 0, 0],
411
+ },
412
+ },
413
+ };
414
+ const method = new CheckAllFirmwareRelease({
415
+ id: 1,
416
+ payload: {
417
+ method: 'checkAllFirmwareRelease',
418
+ platform: 'desktop',
419
+ protocolV2ForceUpdateTargets: ['app_v1', 'coprocessor'],
420
+ },
421
+ });
422
+ method.init();
423
+ method.device = {
424
+ isProtocolV2: () => true,
425
+ features: {
426
+ deviceType: 'pro2',
427
+ serialNo: 'pro2-device-id',
428
+ firmwareVersion: '1.0.0',
429
+ },
430
+ getDeviceState: jest.fn().mockResolvedValue({
431
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
432
+ status: { mode: 'normal' },
433
+ versions: { ...currentVersions, applicationP1: '1.0.0' },
434
+ }),
435
+ } as unknown as CheckAllFirmwareRelease['device'];
436
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(currentRelease);
437
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
438
+
439
+ await expect(method.run()).resolves.toMatchObject({
440
+ targetsToUpdate: ['app_v1', 'coprocessor', 'resource'],
441
+ firmwareUpdatePlan: {
442
+ executor: 'v4',
443
+ platform: 'desktop',
444
+ artifacts: [
445
+ { artifactId: 'component:app_v1', target: 'app_v1' },
446
+ { artifactId: 'component:coprocessor', target: 'coprocessor' },
447
+ { artifactId: 'resource:archive', target: 'resource', container: 'zip' },
448
+ ],
449
+ targetsToUpdate: ['app_v1', 'coprocessor', 'resource'],
450
+ },
451
+ });
452
+ });
453
+
454
+ test('keeps exact force targets but omits a Desktop Plan without device identity', async () => {
455
+ const currentRelease: IFirmwareReleaseInfo = {
456
+ ...release,
457
+ required: false,
458
+ version: [1, 0, 0],
459
+ installOrder: ['applicationP1'],
460
+ components: {
461
+ applicationP1: {
462
+ target: 'APPLICATION_P1',
463
+ url: 'https://example.com/application-p1.okpkg',
464
+ version: [1, 0, 0],
465
+ },
466
+ },
467
+ };
468
+ const method = new CheckAllFirmwareRelease({
469
+ id: 1,
470
+ payload: {
471
+ method: 'checkAllFirmwareRelease',
472
+ platform: 'desktop',
473
+ protocolV2ForceUpdateTargets: ['app_v1'],
474
+ },
475
+ });
476
+ method.init();
477
+ method.device = {
478
+ isProtocolV2: () => true,
479
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0' },
480
+ getDeviceState: jest.fn().mockResolvedValue({
481
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
482
+ status: { mode: 'normal' },
483
+ versions: { ...currentVersions, applicationP1: '1.0.0' },
484
+ }),
485
+ } as unknown as CheckAllFirmwareRelease['device'];
486
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(currentRelease);
487
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
488
+
489
+ await expect(method.run()).resolves.toMatchObject({
490
+ targetsToUpdate: ['app_v1', 'resource'],
491
+ firmwareUpdatePlan: undefined,
492
+ });
493
+ });
494
+
495
+ test('rejects Neo-only unsupported exact secure-element targets', async () => {
496
+ const neoRelease: IFirmwareReleaseInfo = {
497
+ ...release,
498
+ required: false,
499
+ installOrder: ['se03'],
500
+ components: {
501
+ se03: {
502
+ target: 'SE03',
503
+ url: 'https://example.com/se03.okpkg',
504
+ version: [1, 0, 0],
505
+ },
506
+ },
507
+ };
508
+ const method = new CheckAllFirmwareRelease({
509
+ id: 1,
510
+ payload: {
511
+ method: 'checkAllFirmwareRelease',
512
+ protocolV2ForceUpdateTargets: ['se03'],
513
+ },
514
+ });
515
+ method.init();
516
+ method.device = {
517
+ isProtocolV2: () => true,
518
+ features: { deviceType: 'neo', firmwareVersion: '1.0.0' },
519
+ getDeviceState: jest.fn().mockResolvedValue({
520
+ identity: { deviceType: 'neo', firmwareType: EFirmwareType.Universal },
521
+ status: { mode: 'normal' },
522
+ versions: currentVersions,
523
+ }),
524
+ } as unknown as CheckAllFirmwareRelease['device'];
525
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(neoRelease);
526
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
527
+
528
+ await expect(method.run()).rejects.toMatchObject({
529
+ params: { firmwareUpdateCode: 'FirmwarePlanInvalid' },
530
+ });
531
+ });
532
+
533
+ test('builds a V4 plan for Neo and excludes unsupported secure elements when forced', async () => {
534
+ const neoRelease: IFirmwareReleaseInfo = {
535
+ ...release,
536
+ installOrder: ['se01', 'se02', 'se03', 'se04'],
537
+ components: {
538
+ se01: { target: 'SE01', url: 'https://example.com/se01.bin', version: [1, 0, 0] },
539
+ se02: { target: 'SE02', url: 'https://example.com/se02.bin', version: [2, 0, 0] },
540
+ se03: { target: 'SE03', url: 'https://example.com/se03.bin', version: [1, 0, 0] },
541
+ se04: { target: 'SE04', url: 'https://example.com/se04.bin', version: [1, 0, 0] },
542
+ },
543
+ };
544
+ const method = new CheckAllFirmwareRelease({
545
+ id: 1,
546
+ payload: {
547
+ method: 'checkAllFirmwareRelease',
548
+ platform: 'native',
549
+ forceUpdateTargets: ['firmware'],
550
+ },
551
+ });
552
+ method.init();
553
+ method.device = {
554
+ isProtocolV2: () => true,
555
+ features: {
556
+ deviceType: 'neo',
557
+ serialNo: 'neo-device-id',
558
+ firmwareVersion: '1.0.0',
559
+ },
560
+ getDeviceState: jest.fn().mockResolvedValue({
561
+ identity: { deviceType: 'neo', firmwareType: EFirmwareType.Universal },
562
+ status: { mode: 'normal' },
563
+ versions: currentVersions,
564
+ }),
565
+ } as unknown as CheckAllFirmwareRelease['device'];
566
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(neoRelease);
567
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
568
+
569
+ await expect(method.run()).resolves.toMatchObject({
570
+ targetsToUpdate: ['se01', 'se02', 'resource'],
571
+ firmwareUpdatePlan: {
572
+ executor: 'v4',
573
+ deviceModel: 'neo',
574
+ platform: 'native',
575
+ targetsToUpdate: ['se01', 'se02', 'resource'],
576
+ },
577
+ });
578
+ });
579
+
580
+ test('rejects a forced Protocol V2 resource target without a configured archive', async () => {
581
+ const method = new CheckAllFirmwareRelease({
582
+ id: 1,
583
+ payload: {
584
+ method: 'checkAllFirmwareRelease',
585
+ forceUpdateTargets: ['resource'],
586
+ },
587
+ });
588
+ method.init();
589
+ method.device = {
590
+ isProtocolV2: () => true,
591
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0' },
592
+ getDeviceState: jest.fn().mockResolvedValue({
593
+ identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
594
+ status: { mode: 'normal' },
595
+ versions: currentVersions,
596
+ }),
597
+ } as unknown as CheckAllFirmwareRelease['device'];
598
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
599
+ jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(undefined);
600
+
601
+ await expect(method.run()).rejects.toMatchObject({
602
+ params: { firmwareUpdateCode: 'FirmwarePlanInvalid' },
603
+ });
604
+ });
605
+
347
606
  test('requests and compares firmware hashes only when explicitly enabled', async () => {
348
607
  const packageSet: IFirmwareReleaseInfo = {
349
608
  ...release,
350
609
  required: false,
351
- resourceBundles: undefined,
352
610
  installOrder: ['applicationP1'],
353
611
  components: {
354
612
  applicationP1: {
@@ -467,6 +725,18 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
467
725
  resourceStatus: 'unknown',
468
726
  resourcePreparationRequired: true,
469
727
  targetsToUpdate: ['resource'],
728
+ firmwareUpdatePlan: {
729
+ executor: 'v4',
730
+ artifacts: [
731
+ {
732
+ artifactId: 'resource:archive',
733
+ target: 'resource',
734
+ role: 'resourceBundle',
735
+ container: 'zip',
736
+ },
737
+ ],
738
+ targetsToUpdate: ['resource'],
739
+ },
470
740
  });
471
741
  });
472
742
 
@@ -477,6 +747,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
477
747
  await api.checkAllFirmwareRelease('pro2-connect-id', {
478
748
  checkFirmwareHash: true,
479
749
  firmwareType: EFirmwareType.Universal,
750
+ protocolV2ForceUpdateTargets: ['app_v1'],
480
751
  retryCount: 0,
481
752
  });
482
753
 
@@ -484,6 +755,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
484
755
  connectId: 'pro2-connect-id',
485
756
  checkFirmwareHash: true,
486
757
  firmwareType: EFirmwareType.Universal,
758
+ protocolV2ForceUpdateTargets: ['app_v1'],
487
759
  retryCount: 0,
488
760
  method: 'checkAllFirmwareRelease',
489
761
  });
@@ -22,6 +22,10 @@ const release: IFirmwareReleaseInfo = {
22
22
  'zh-CN': '更新',
23
23
  'en-US': 'Update',
24
24
  },
25
+ bootloaderChangelog: {
26
+ 'zh-CN': '旧版 Bootloader 更新',
27
+ 'en-US': 'Legacy bootloader update',
28
+ },
25
29
  installOrder: ['bootloader', 'applicationP1', 'coprocessor'],
26
30
  components: {
27
31
  bootloader: {
@@ -127,6 +131,7 @@ describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', dev
127
131
  await expect(bootloaderMethod.run()).resolves.toMatchObject({
128
132
  status: 'outdated',
129
133
  shouldUpdate: true,
134
+ changelog: [release.changelog],
130
135
  release: {
131
136
  protocol: 'V2',
132
137
  configKey: 'bootloader',
@@ -134,6 +139,7 @@ describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', dev
134
139
  target: 'BOOTLOADER',
135
140
  url: 'https://example.com/bootloader.okpkg',
136
141
  version: [2, 0, 0],
142
+ changelog: release.changelog,
137
143
  },
138
144
  });
139
145
  await expect(allMethod.run()).resolves.toMatchObject({
@@ -0,0 +1,105 @@
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(host.componentArtifacts.boot?.size).toBe(componentBinary.byteLength);
91
+ const opened = await artifactReader!.open({
92
+ artifactRef: host.componentArtifacts.boot!.artifactRef,
93
+ });
94
+ const chunk = await artifactReader!.read({
95
+ readerId: opened.readerId,
96
+ offset: 1,
97
+ length: 2,
98
+ });
99
+ expect(Array.from(new Uint8Array(chunk.data))).toEqual([2, 3]);
100
+ await artifactReader!.close({ readerId: opened.readerId });
101
+
102
+ host.release();
103
+ expect(unregisterFirmwareUpdateHostBinding).toHaveBeenCalledWith(7);
104
+ });
105
+ });
@@ -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(() =>