@onekeyfe/hd-core 1.2.0-alpha.97 → 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 (98) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +112 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
  9. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  10. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  12. package/__tests__/protocol-v2-resources.test.ts +148 -232
  13. package/__tests__/protocol-v2.test.ts +1236 -516
  14. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  15. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  16. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  17. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  19. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  20. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  21. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +8 -8
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  28. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  31. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  32. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  34. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  35. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  36. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  37. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  38. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  39. package/dist/api/firmware/getBinary.d.ts +0 -1
  40. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  41. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  42. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  43. package/dist/data-manager/DataManager.d.ts +4 -3
  44. package/dist/data-manager/DataManager.d.ts.map +1 -1
  45. package/dist/index.d.ts +108 -114
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1633 -967
  48. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  51. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  52. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  53. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  54. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  55. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  59. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  60. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/types/api/export.d.ts +1 -1
  62. package/dist/types/api/export.d.ts.map +1 -1
  63. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  64. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  66. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  67. package/dist/types/settings.d.ts +35 -36
  68. package/dist/types/settings.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +108 -80
  71. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  72. package/src/api/CheckBootloaderRelease.ts +35 -3
  73. package/src/api/CheckFirmwareRelease.ts +35 -7
  74. package/src/api/FirmwareUpdateV2.ts +108 -73
  75. package/src/api/FirmwareUpdateV3.ts +101 -56
  76. package/src/api/FirmwareUpdateV4.ts +885 -403
  77. package/src/api/UploadPortfolio.ts +18 -0
  78. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  79. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  80. package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
  81. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  82. package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
  83. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
  84. package/src/api/firmware/protocolV2Release.ts +168 -0
  85. package/src/data-manager/DataManager.ts +35 -19
  86. package/src/index.ts +10 -0
  87. package/src/protocols/protocol-v2/resources.ts +157 -341
  88. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  89. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  90. package/src/types/api/checkBootloaderRelease.ts +2 -6
  91. package/src/types/api/checkFirmwareRelease.ts +2 -13
  92. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  93. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  94. package/src/types/api/export.ts +6 -1
  95. package/src/types/api/firmwareUpdate.ts +12 -17
  96. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  97. package/src/types/settings.ts +35 -61
  98. package/src/utils/deviceFeaturesUtils.ts +2 -2
@@ -10,13 +10,14 @@ import {
10
10
  getDeviceFirmwareVersion,
11
11
  } from '../../utils/deviceVersionUtils';
12
12
 
13
+ import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
13
14
  import type {
14
15
  FirmwareUpdatePlan,
15
16
  FirmwareUpdatePlanArtifact,
16
17
  FirmwareUpdatePlanForceTarget,
17
18
  FirmwareUpdatePlanTarget,
18
19
  } from '../../types/api/firmwareUpdatePlan';
19
- import type { Features } from '../../types';
20
+ import type { Features, IProtocolV2ResourceSource } from '../../types';
20
21
 
21
22
  type FirmwareUpdatePlatform = 'native' | 'desktop' | 'ext' | 'web' | 'web-embed';
22
23
 
@@ -30,7 +31,6 @@ type ReleaseRecord = {
30
31
  version?: unknown;
31
32
  components?: unknown;
32
33
  installOrder?: unknown;
33
- resourceBundles?: unknown;
34
34
  fingerprint?: unknown;
35
35
  fingerprintWeb?: unknown;
36
36
  expectedSize?: unknown;
@@ -62,6 +62,11 @@ const PROTOCOL_V2_TARGETS: Readonly<
62
62
  SE04: 'se04',
63
63
  };
64
64
 
65
+ const PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS = new Set<FirmwareUpdateV4Target>([
66
+ ...Object.values(PROTOCOL_V2_TARGETS),
67
+ 'resource',
68
+ ]);
69
+
65
70
  const asRecord = (value: unknown): Record<string, unknown> | undefined =>
66
71
  value && typeof value === 'object' && !Array.isArray(value)
67
72
  ? (value as Record<string, unknown>)
@@ -90,7 +95,7 @@ const asIntegrity = ({
90
95
  }: {
91
96
  size: unknown;
92
97
  sha256: unknown;
93
- }): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
98
+ }): Partial<Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'>> => {
94
99
  const sha256Value =
95
100
  typeof value === 'string' && /^[a-f0-9]{64}$/iu.test(value) ? value.toLowerCase() : undefined;
96
101
  return {
@@ -167,6 +172,20 @@ const planError = (message: string): never => {
167
172
  });
168
173
  };
169
174
 
175
+ const requireArtifactIntegrity = (
176
+ input: Parameters<typeof asIntegrity>[0],
177
+ label: string
178
+ ): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
179
+ const integrity = asIntegrity(input);
180
+ if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
181
+ return planError(`${label} integrity metadata is invalid`);
182
+ }
183
+ return {
184
+ expectedSize: integrity.expectedSize,
185
+ expectedSha256: integrity.expectedSha256,
186
+ };
187
+ };
188
+
170
189
  const FIRMWARE_UPDATE_PLAN_FORCE_TARGETS = new Set<FirmwareUpdatePlanForceTarget>([
171
190
  'firmware',
172
191
  'ble',
@@ -191,6 +210,23 @@ export const validateFirmwareUpdatePlanForceTargets = (
191
210
  return [...value] as FirmwareUpdatePlanForceTarget[];
192
211
  };
193
212
 
213
+ export const validateProtocolV2FirmwareUpdateTargets = (
214
+ value: unknown
215
+ ): FirmwareUpdateV4Target[] => {
216
+ if (value === undefined) {
217
+ return [];
218
+ }
219
+ if (
220
+ !Array.isArray(value) ||
221
+ value.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
222
+ value.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
223
+ new Set(value).size !== value.length
224
+ ) {
225
+ return planError('Protocol V2 firmware update targets are invalid');
226
+ }
227
+ return [...value] as FirmwareUpdateV4Target[];
228
+ };
229
+
194
230
  const assertExactKeys = (
195
231
  value: Record<string, unknown>,
196
232
  required: readonly string[],
@@ -287,8 +323,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
287
323
  }
288
324
  assertExactKeys(
289
325
  artifact,
290
- ['artifactId', 'role', 'target', 'url', 'container'],
291
- ['logicalName', 'expectedSize', 'expectedSha256', 'targetVersion']
326
+ ['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
327
+ ['logicalName', 'targetVersion']
292
328
  );
293
329
  const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
294
330
  if (artifactIds.has(artifactId)) {
@@ -307,16 +343,12 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
307
343
  if (artifact.logicalName !== undefined) {
308
344
  assertBoundedString(artifact.logicalName, 'logical name', 256);
309
345
  }
310
- if (
311
- artifact.expectedSize !== undefined &&
312
- (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
313
- ) {
346
+ if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
314
347
  return planError('Firmware update plan artifact size is invalid');
315
348
  }
316
349
  if (
317
- artifact.expectedSha256 !== undefined &&
318
- (typeof artifact.expectedSha256 !== 'string' ||
319
- !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
350
+ typeof artifact.expectedSha256 !== 'string' ||
351
+ !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
320
352
  ) {
321
353
  return planError('Firmware update plan artifact digest is invalid');
322
354
  }
@@ -351,13 +383,11 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
351
383
  plan.targetsToUpdate.some(target =>
352
384
  legacyOnlyTargets.has(target as FirmwareUpdatePlanTarget)
353
385
  )) ||
354
- // Only executor v2 services identity-less bootloader recovery (classic family);
355
- // Pro (v3) and Pro2 (v4) report a serial even in bootloader mode, so their plans
356
- // must always carry a real identity. Degraded v2 plans are bound to the live
357
- // device at install time instead.
386
+ // V2 仅在 bootloader 恢复时允许缺少身份;V4 设备在尚未写入序列号时也可升级。
387
+ // 这类 Plan 仍通过设备型号、目标、工件摘要与 preparedPlanDigest 进行约束。
358
388
  (plan.artifacts.length > 0 &&
359
389
  (plan.platform === 'native' || plan.platform === 'desktop') &&
360
- plan.executor !== 'v2' &&
390
+ plan.executor === 'v3' &&
361
391
  plan.deviceIdentity === 'unavailable')
362
392
  ) {
363
393
  return planError('Firmware update plan executor contract is invalid');
@@ -405,7 +435,7 @@ const selectResourceUrl = ({
405
435
 
406
436
  const getExecutor = (features: Features): FirmwareUpdatePlan['executor'] => {
407
437
  const deviceType = getDeviceType(features);
408
- if (deviceType === EDeviceType.Pro2) {
438
+ if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) {
409
439
  return 'v4';
410
440
  }
411
441
  if (
@@ -421,10 +451,10 @@ const buildProtocolV2Artifacts = (
421
451
  release: ReleaseRecord,
422
452
  {
423
453
  includeComponents = true,
424
- includeResources = true,
454
+ componentTargets: selectedComponentTargets,
425
455
  }: {
426
456
  includeComponents?: boolean;
427
- includeResources?: boolean;
457
+ componentTargets?: ReadonlySet<FirmwareUpdatePlanTarget>;
428
458
  } = {}
429
459
  ): {
430
460
  artifacts: FirmwareUpdatePlanArtifact[];
@@ -445,12 +475,21 @@ const buildProtocolV2Artifacts = (
445
475
  includeComponents && components
446
476
  ? [...installOrder, ...Object.keys(components).filter(key => !installOrder.includes(key))]
447
477
  : [];
478
+ const selectedComponentKeys = selectedComponentTargets
479
+ ? componentKeys.filter(key => {
480
+ const component = asRecord(components?.[key]);
481
+ const targetName = asString(component?.target)?.toUpperCase();
482
+ const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
483
+ return !!target && selectedComponentTargets.has(target);
484
+ })
485
+ : componentKeys;
448
486
  const artifacts: FirmwareUpdatePlanArtifact[] = [];
449
487
  const targets: FirmwareUpdatePlanTarget[] = [];
450
- const componentTargets = new Set<FirmwareUpdatePlanTarget>();
451
- for (const key of componentKeys) {
488
+ const componentTargetSet = new Set<FirmwareUpdatePlanTarget>();
489
+ for (const key of selectedComponentKeys) {
452
490
  const component = asRecord(components?.[key]);
453
491
  const targetName = asString(component?.target)?.toUpperCase();
492
+ const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
454
493
  if (targetName === 'ROMLOADER') {
455
494
  throw ERRORS.TypedError(
456
495
  HardwareErrorCode.RuntimeError,
@@ -458,7 +497,6 @@ const buildProtocolV2Artifacts = (
458
497
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
459
498
  );
460
499
  }
461
- const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
462
500
  if (!component || !target) {
463
501
  throw ERRORS.TypedError(
464
502
  HardwareErrorCode.RuntimeError,
@@ -466,14 +504,18 @@ const buildProtocolV2Artifacts = (
466
504
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
467
505
  );
468
506
  }
469
- if (componentTargets.has(target)) {
507
+ if (componentTargetSet.has(target)) {
470
508
  throw ERRORS.TypedError(
471
509
  HardwareErrorCode.RuntimeError,
472
510
  `Protocol V2 component ${key} duplicates target ${target}`,
473
511
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
474
512
  );
475
513
  }
476
- componentTargets.add(target);
514
+ componentTargetSet.add(target);
515
+ const integrity = requireArtifactIntegrity(
516
+ { size: component.expectedSize, sha256: component.fingerprint },
517
+ `Protocol V2 component ${key}`
518
+ );
477
519
  artifacts.push({
478
520
  artifactId: `component:${target}`,
479
521
  role: 'component',
@@ -481,52 +523,12 @@ const buildProtocolV2Artifacts = (
481
523
  url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
482
524
  container: 'raw',
483
525
  logicalName: key,
484
- ...asIntegrity({
485
- size: component.expectedSize,
486
- sha256: component.fingerprint,
487
- }),
526
+ ...integrity,
488
527
  ...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
489
528
  });
490
529
  targets.push(target);
491
530
  }
492
531
 
493
- const bundles =
494
- includeResources && Array.isArray(release.resourceBundles) ? release.resourceBundles : [];
495
- const resourceBundleNames = new Set<string>();
496
- for (const value of bundles) {
497
- const bundle = asRecord(value);
498
- const name = asString(bundle?.name);
499
- if (!bundle || !name) {
500
- throw ERRORS.TypedError(
501
- HardwareErrorCode.RuntimeError,
502
- 'Protocol V2 resource bundle is invalid',
503
- { firmwareUpdateCode: 'FirmwarePlanInvalid' }
504
- );
505
- }
506
- if (resourceBundleNames.has(name)) {
507
- throw ERRORS.TypedError(
508
- HardwareErrorCode.RuntimeError,
509
- `Protocol V2 resource bundle duplicates name ${name}`,
510
- { firmwareUpdateCode: 'FirmwarePlanInvalid' }
511
- );
512
- }
513
- resourceBundleNames.add(name);
514
- artifacts.push({
515
- artifactId: `resourceBundle:${name}`,
516
- role: 'resourceBundle',
517
- target: 'resource',
518
- url: assertArtifactUrl(bundle.url, `Protocol V2 resource bundle ${name}`),
519
- container: 'raw',
520
- logicalName: name,
521
- ...asIntegrity({
522
- size: bundle.expectedSize,
523
- sha256: bundle.fingerprint,
524
- }),
525
- });
526
- }
527
- if (bundles.length > 0) {
528
- targets.push('resource');
529
- }
530
532
  return { artifacts, targets: [...new Set(targets)] };
531
533
  };
532
534
 
@@ -587,6 +589,176 @@ const assertForcedTargetsRepresented = ({
587
589
  }
588
590
  };
589
591
 
592
+ const finalizeFirmwareUpdatePlan = ({
593
+ features,
594
+ firmwareType,
595
+ platform,
596
+ artifacts,
597
+ targetsToUpdate,
598
+ }: {
599
+ features: Features;
600
+ firmwareType: EFirmwareType;
601
+ platform: FirmwareUpdatePlatform;
602
+ artifacts: FirmwareUpdatePlanArtifact[];
603
+ targetsToUpdate: FirmwareUpdatePlanTarget[];
604
+ }): FirmwareUpdatePlan => {
605
+ const executor = getExecutor(features);
606
+ const bootloaderMode = resolveDeviceBootloaderMode(features);
607
+ // 'unavailable' is the reserved degraded-identity sentinel; a device-reported
608
+ // serial must never be able to collide with it.
609
+ const reportedIdentity = getDeviceUUID(features);
610
+ const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
611
+ // Classic 系列仅在 bootloader 恢复时允许缺少身份;部分尚未完成产线写号的
612
+ // Pro2/Neo(V4)在正常模式下也可能没有序列号,因此允许使用降级身份。
613
+ const allowsUnavailableIdentity = (bootloaderMode && executor === 'v2') || executor === 'v4';
614
+ if (
615
+ artifacts.length > 0 &&
616
+ (platform === 'native' || platform === 'desktop') &&
617
+ !deviceIdentity &&
618
+ !allowsUnavailableIdentity
619
+ ) {
620
+ throw ERRORS.TypedError(
621
+ HardwareErrorCode.RuntimeError,
622
+ 'Prepared firmware update requires a stable device identity',
623
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
624
+ );
625
+ }
626
+ const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
627
+ schemaVersion: 2,
628
+ executor,
629
+ deviceIdentity: deviceIdentity || 'unavailable',
630
+ deviceModel: String(getDeviceType(features)),
631
+ firmwareType,
632
+ platform,
633
+ artifacts,
634
+ targetsToUpdate,
635
+ };
636
+ return assertFirmwareUpdatePlan({
637
+ ...planWithoutDigest,
638
+ planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
639
+ });
640
+ };
641
+
642
+ export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
643
+ artifactId: string;
644
+ target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
645
+ container: 'raw' | 'zip';
646
+ logicalName: string;
647
+ expectedSize: number;
648
+ expectedSha256: string;
649
+ targetVersion?: string;
650
+ };
651
+
652
+ export const buildProtocolV2LocalFirmwareUpdatePlan = ({
653
+ features,
654
+ firmwareType,
655
+ platform,
656
+ artifacts,
657
+ }: {
658
+ features: Features;
659
+ firmwareType: EFirmwareType;
660
+ platform: FirmwareUpdatePlatform;
661
+ artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
662
+ }): FirmwareUpdatePlan => {
663
+ if (artifacts.length === 0) {
664
+ return planError('Protocol V2 local firmware plan has no artifacts');
665
+ }
666
+ const plan = finalizeFirmwareUpdatePlan({
667
+ features,
668
+ firmwareType,
669
+ platform,
670
+ artifacts: artifacts.map(artifact => ({
671
+ ...artifact,
672
+ role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
673
+ url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
674
+ })),
675
+ targetsToUpdate: artifacts.map(artifact => artifact.target),
676
+ });
677
+ if (plan.executor !== 'v4') {
678
+ return planError('Protocol V2 local firmware plan requires executor v4');
679
+ }
680
+ return plan;
681
+ };
682
+
683
+ export const buildProtocolV2FirmwareUpdatePlan = ({
684
+ features,
685
+ firmwareType,
686
+ platform,
687
+ release,
688
+ targetsToUpdate,
689
+ forceUpdateTargets,
690
+ resourceArchive,
691
+ }: {
692
+ features: Features;
693
+ firmwareType: EFirmwareType;
694
+ platform: FirmwareUpdatePlatform;
695
+ release: ReleaseRecord | undefined;
696
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
697
+ forceUpdateTargets?: FirmwareUpdatePlanForceTarget[];
698
+ resourceArchive: IProtocolV2ResourceSource | undefined;
699
+ }): FirmwareUpdatePlan => {
700
+ const validatedForceTargets = validateFirmwareUpdatePlanForceTargets(forceUpdateTargets);
701
+ if (validatedForceTargets.some(target => target === 'ble' || target === 'bootloader')) {
702
+ planError('Protocol V2 does not support forced BLE or legacy bootloader targets');
703
+ }
704
+
705
+ const requestedComponentTargets = new Set<FirmwareUpdatePlanTarget>(
706
+ targetsToUpdate.filter(target => target !== 'resource')
707
+ );
708
+ if (
709
+ getDeviceType(features) === EDeviceType.Neo &&
710
+ (requestedComponentTargets.has('se03') || requestedComponentTargets.has('se04'))
711
+ ) {
712
+ planError('Neo does not support Protocol V2 targets se03 or se04');
713
+ }
714
+ const protocolV2 = buildProtocolV2Artifacts(release ?? {}, {
715
+ includeComponents: requestedComponentTargets.size > 0,
716
+ componentTargets: requestedComponentTargets,
717
+ });
718
+ const resourceRequested = targetsToUpdate.includes('resource');
719
+ if (resourceRequested) {
720
+ const archive = resourceArchive;
721
+ if (!archive) {
722
+ return planError('Protocol V2 resource target has no resource archive');
723
+ }
724
+ const integrity = requireArtifactIntegrity(
725
+ { size: archive.archiveSize, sha256: archive.archiveSha256 },
726
+ 'Protocol V2 resource archive'
727
+ );
728
+ protocolV2.artifacts.push({
729
+ artifactId: 'resource:archive',
730
+ role: 'resourceBundle',
731
+ target: 'resource',
732
+ url: assertArtifactUrl(archive.archiveUrl, 'Protocol V2 resource archive'),
733
+ container: 'zip',
734
+ logicalName: 'protocol-v2-resource-archive',
735
+ ...integrity,
736
+ });
737
+ protocolV2.targets.push('resource');
738
+ }
739
+ const representedTargets = new Set(protocolV2.targets);
740
+ const missingTarget = Array.from(requestedComponentTargets).find(
741
+ target => !representedTargets.has(target)
742
+ );
743
+ if (missingTarget) {
744
+ planError(`Protocol V2 release does not contain requested target ${missingTarget}`);
745
+ }
746
+ if (validatedForceTargets.includes('firmware') && protocolV2.targets.length === 0) {
747
+ planError('Forced firmware update target firmware is not represented by the plan');
748
+ }
749
+ if (validatedForceTargets.includes('resource') && !resourceArchive) {
750
+ planError('Forced firmware update target resource has no Protocol V2 resource archive');
751
+ }
752
+
753
+ return finalizeFirmwareUpdatePlan({
754
+ features,
755
+ firmwareType,
756
+ platform,
757
+ artifacts: protocolV2.artifacts,
758
+ targetsToUpdate: protocolV2.targets,
759
+ });
760
+ };
761
+
590
762
  export const buildFirmwareUpdatePlan = ({
591
763
  features,
592
764
  firmwareType,
@@ -623,7 +795,11 @@ export const buildFirmwareUpdatePlan = ({
623
795
  !!asRelease(release);
624
796
  const shouldUpdateFirmware =
625
797
  isUpgrade(firmware) || isRecoveryInstall(firmware) || forcedTargets.has('firmware');
626
- const shouldUpdateResource = isUpgrade(firmware) || forcedTargets.has('resource');
798
+ // V2 cannot write legacy resources once the device is already in bootloader
799
+ // recovery. Do not approve an artifact that its executor cannot apply.
800
+ const shouldUpdateResource =
801
+ !(executor === 'v2' && bootloaderMode) &&
802
+ (isUpgrade(firmware) || forcedTargets.has('resource'));
627
803
 
628
804
  if (
629
805
  executor === 'v4' &&
@@ -635,25 +811,29 @@ export const buildFirmwareUpdatePlan = ({
635
811
  if (executor === 'v4' && (shouldUpdateFirmware || shouldUpdateResource)) {
636
812
  const protocolV2 = buildProtocolV2Artifacts(firmwareRelease ?? {}, {
637
813
  includeComponents: shouldUpdateFirmware,
638
- includeResources: shouldUpdateResource,
639
814
  });
640
815
  artifacts = protocolV2.artifacts;
641
816
  targetsToUpdate = protocolV2.targets;
642
817
  } else {
643
818
  if (isUpgrade(bootloader) || forcedTargets.has('bootloader')) {
819
+ const bootloaderRelease = asRelease(bootloader);
820
+ const integrity = requireArtifactIntegrity(
821
+ {
822
+ size: bootloaderRelease?.bootloaderExpectedSize,
823
+ sha256: bootloaderRelease?.bootloaderFingerprint,
824
+ },
825
+ 'Bootloader release'
826
+ );
644
827
  artifacts.push({
645
828
  artifactId: 'bootloader',
646
829
  role: 'bootloader',
647
830
  target: 'bootloader',
648
- url: assertArtifactUrl(asRelease(bootloader)?.bootloaderResource, 'Bootloader release'),
831
+ url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
649
832
  container: 'raw',
650
- ...asIntegrity({
651
- size: asRelease(bootloader)?.bootloaderExpectedSize,
652
- sha256: asRelease(bootloader)?.bootloaderFingerprint,
653
- }),
654
- ...(asVersion(asRelease(bootloader)?.bootloaderVersion)
833
+ ...integrity,
834
+ ...(asVersion(bootloaderRelease?.bootloaderVersion)
655
835
  ? {
656
- targetVersion: asVersion(asRelease(bootloader)?.bootloaderVersion),
836
+ targetVersion: asVersion(bootloaderRelease?.bootloaderVersion),
657
837
  }
658
838
  : {}),
659
839
  });
@@ -665,16 +845,17 @@ export const buildFirmwareUpdatePlan = ({
665
845
  });
666
846
  }
667
847
  if (shouldUpdateFirmware) {
848
+ const integrity = requireArtifactIntegrity(
849
+ { size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
850
+ 'Firmware release'
851
+ );
668
852
  artifacts.push({
669
853
  artifactId: 'firmware',
670
854
  role: 'firmware',
671
855
  target: 'firmware',
672
856
  url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
673
857
  container: 'raw',
674
- ...asIntegrity({
675
- size: firmwareRelease.expectedSize,
676
- sha256: firmwareRelease.fingerprint,
677
- }),
858
+ ...integrity,
678
859
  ...(asVersion(firmwareRelease.version)
679
860
  ? { targetVersion: asVersion(firmwareRelease.version) }
680
861
  : {}),
@@ -687,13 +868,8 @@ export const buildFirmwareUpdatePlan = ({
687
868
  platform,
688
869
  });
689
870
  if (resourceUrl) {
690
- artifacts.push({
691
- artifactId: 'resource',
692
- role: 'resource',
693
- target: 'resource',
694
- url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
695
- container: 'zip',
696
- ...asIntegrity({
871
+ const integrity = requireArtifactIntegrity(
872
+ {
697
873
  size:
698
874
  resourceUrl === asString(firmwareRelease.fullResource)
699
875
  ? firmwareRelease.fullResourceExpectedSize
@@ -702,23 +878,36 @@ export const buildFirmwareUpdatePlan = ({
702
878
  resourceUrl === asString(firmwareRelease.fullResource)
703
879
  ? firmwareRelease.fullResourceFingerprint
704
880
  : firmwareRelease.resourceFingerprint,
705
- }),
881
+ },
882
+ 'Legacy resource archive'
883
+ );
884
+ artifacts.push({
885
+ artifactId: 'resource',
886
+ role: 'resource',
887
+ target: 'resource',
888
+ url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
889
+ container: 'zip',
890
+ ...integrity,
706
891
  });
707
892
  }
708
893
  }
709
894
  }
710
895
  if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
711
896
  const bleRelease = asRelease(ble);
897
+ const integrity = requireArtifactIntegrity(
898
+ {
899
+ size: bleRelease?.expectedSize,
900
+ sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
901
+ },
902
+ 'BLE release'
903
+ );
712
904
  artifacts.push({
713
905
  artifactId: 'ble',
714
906
  role: 'ble',
715
907
  target: 'ble',
716
908
  url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
717
909
  container: 'raw',
718
- ...asIntegrity({
719
- size: bleRelease?.expectedSize,
720
- sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
721
- }),
910
+ ...integrity,
722
911
  ...(asVersion(bleRelease?.version)
723
912
  ? { targetVersion: asVersion(bleRelease?.version) }
724
913
  : {}),
@@ -733,40 +922,11 @@ export const buildFirmwareUpdatePlan = ({
733
922
  artifacts,
734
923
  targetsToUpdate,
735
924
  });
736
-
737
- // 'unavailable' is the reserved degraded-identity sentinel; a device-reported
738
- // serial must never be able to collide with it.
739
- const reportedIdentity = getDeviceUUID(features);
740
- const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
741
- // Bootloader-mode classic-family devices (executor v2, e.g. classic1s) cannot report
742
- // a serial number, so their recovery plans fall back to the degraded 'unavailable'
743
- // identity. Binding is then enforced against the live device at install time instead
744
- // (assertFirmwareUpdatePreparedPlanDeviceIdentity), and plan integrity by the digest.
745
- // Pro (v3) and Pro2 (v4) report a serial even in bootloader mode and stay strict.
746
- if (
747
- artifacts.length > 0 &&
748
- (platform === 'native' || platform === 'desktop') &&
749
- !deviceIdentity &&
750
- !(bootloaderMode && executor === 'v2')
751
- ) {
752
- throw ERRORS.TypedError(
753
- HardwareErrorCode.RuntimeError,
754
- 'Prepared firmware update requires a stable device identity',
755
- { firmwareUpdateCode: 'FirmwarePlanInvalid' }
756
- );
757
- }
758
- const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
759
- schemaVersion: 2,
760
- executor,
761
- deviceIdentity: deviceIdentity || 'unavailable',
762
- deviceModel: String(getDeviceType(features)),
925
+ return finalizeFirmwareUpdatePlan({
926
+ features,
763
927
  firmwareType,
764
928
  platform,
765
929
  artifacts,
766
930
  targetsToUpdate,
767
- };
768
- return assertFirmwareUpdatePlan({
769
- ...planWithoutDigest,
770
- planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
771
931
  });
772
932
  };
@@ -278,7 +278,14 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
278
278
  artifactIds.add(artifact.artifactId);
279
279
  artifactTargets.add(artifact.target);
280
280
  assertFirmwareArtifactReference(artifact.artifact);
281
- artifact.materializedEntries?.forEach(assertPreparedEntry);
281
+ const materializedEntryNames =
282
+ artifact.materializedEntries?.map(entry => {
283
+ assertPreparedEntry(entry);
284
+ return getFirmwareUpdateResourceName(entry.entryName).toLowerCase();
285
+ }) ?? [];
286
+ if (new Set(materializedEntryNames).size !== materializedEntryNames.length) {
287
+ return preparedPlanError('Firmware prepared plan contains duplicate entry names');
288
+ }
282
289
  }
283
290
  if (
284
291
  preparedPlan.targetsToUpdate.some(target => !FIRMWARE_UPDATE_PLAN_TARGETS.has(target)) ||
@@ -295,6 +302,23 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
295
302
  return preparedPlan;
296
303
  };
297
304
 
305
+ export const getFirmwareUpdatePreparedRawArtifact = ({
306
+ preparedPlan: value,
307
+ target,
308
+ role,
309
+ }: {
310
+ preparedPlan: unknown;
311
+ target: FirmwareUpdatePreparedArtifact['target'];
312
+ role: FirmwareUpdatePreparedArtifact['role'];
313
+ }): FirmwareUpdatePreparedArtifact => {
314
+ const preparedPlan = validateFirmwareUpdatePreparedPlan(value);
315
+ const artifacts = preparedPlan.artifacts.filter(artifact => artifact.target === target);
316
+ if (artifacts.length !== 1 || artifacts[0].role !== role || artifacts[0].container !== 'raw') {
317
+ return preparedPlanError(`Firmware prepared plan ${target} artifact is invalid`);
318
+ }
319
+ return artifacts[0];
320
+ };
321
+
298
322
  /**
299
323
  * Identity observed on the live device while a degraded recovery plan runs, keyed by
300
324
  * the plan's opaque lease. Bootloader recovery starts with no serial and the device
@@ -342,8 +366,8 @@ export const assertFirmwareUpdatePreparedPlanDeviceIdentity = ({
342
366
  }
343
367
  const pinnedIdentity = degradedPlanIdentityPins.get(preparedPlan.leaseRef);
344
368
  if (!deviceIdentity) {
345
- // Still identity-less: only legitimate while the device sits in bootloader.
346
- if (bootloaderMode !== true) {
369
+ // V4 设备可能尚未写入序列号;V2 则只允许 bootloader 恢复流程缺少身份。
370
+ if (preparedPlan.executor !== 'v4' && bootloaderMode !== true) {
347
371
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
348
372
  }
349
373
  return preparedPlan;