@onekeyfe/hd-core 1.2.0-alpha.97 → 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 (103) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +411 -86
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +126 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -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-bootloader-poll.test.ts +65 -2
  9. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  12. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  14. package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
  15. package/__tests__/protocol-v2-resources.test.ts +148 -232
  16. package/__tests__/protocol-v2.test.ts +1326 -510
  17. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  18. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  19. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  20. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  21. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  22. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  23. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  24. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  26. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +11 -9
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  33. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  34. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  35. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  37. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  38. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  39. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  40. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  41. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +7 -1
  42. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  43. package/dist/api/firmware/getBinary.d.ts +0 -1
  44. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  45. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  46. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  47. package/dist/data-manager/DataManager.d.ts +4 -3
  48. package/dist/data-manager/DataManager.d.ts.map +1 -1
  49. package/dist/index.d.ts +108 -114
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1825 -1006
  52. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  53. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  54. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  55. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  59. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  60. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  61. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  62. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  63. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  64. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  65. package/dist/types/api/export.d.ts +1 -1
  66. package/dist/types/api/export.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  68. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  71. package/dist/types/settings.d.ts +35 -36
  72. package/dist/types/settings.d.ts.map +1 -1
  73. package/package.json +4 -4
  74. package/src/api/CheckAllFirmwareRelease.ts +113 -80
  75. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  76. package/src/api/CheckBootloaderRelease.ts +35 -3
  77. package/src/api/CheckFirmwareRelease.ts +35 -7
  78. package/src/api/FirmwareUpdateV2.ts +231 -138
  79. package/src/api/FirmwareUpdateV3.ts +101 -56
  80. package/src/api/FirmwareUpdateV4.ts +936 -408
  81. package/src/api/UploadPortfolio.ts +18 -0
  82. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  83. package/src/api/firmware/FirmwareHostBinding.ts +26 -3
  84. package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
  85. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  86. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
  87. package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
  88. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
  89. package/src/api/firmware/protocolV2Release.ts +168 -0
  90. package/src/data-manager/DataManager.ts +35 -19
  91. package/src/index.ts +10 -0
  92. package/src/protocols/protocol-v2/resources.ts +157 -341
  93. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  94. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  95. package/src/types/api/checkBootloaderRelease.ts +2 -6
  96. package/src/types/api/checkFirmwareRelease.ts +2 -13
  97. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  98. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  99. package/src/types/api/export.ts +6 -1
  100. package/src/types/api/firmwareUpdate.ts +12 -17
  101. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  102. package/src/types/settings.ts +35 -61
  103. 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,28 @@ 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 (!Array.isArray(value)) {
220
+ return planError('Protocol V2 firmware update targets are invalid');
221
+ }
222
+ const normalizedTargets = value.map(target =>
223
+ target === 'boot_resources' ? 'resource' : target
224
+ );
225
+ if (
226
+ normalizedTargets.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
227
+ normalizedTargets.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
228
+ new Set(normalizedTargets).size !== normalizedTargets.length
229
+ ) {
230
+ return planError('Protocol V2 firmware update targets are invalid');
231
+ }
232
+ return normalizedTargets as FirmwareUpdateV4Target[];
233
+ };
234
+
194
235
  const assertExactKeys = (
195
236
  value: Record<string, unknown>,
196
237
  required: readonly string[],
@@ -287,8 +328,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
287
328
  }
288
329
  assertExactKeys(
289
330
  artifact,
290
- ['artifactId', 'role', 'target', 'url', 'container'],
291
- ['logicalName', 'expectedSize', 'expectedSha256', 'targetVersion']
331
+ ['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
332
+ ['logicalName', 'targetVersion']
292
333
  );
293
334
  const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
294
335
  if (artifactIds.has(artifactId)) {
@@ -307,16 +348,12 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
307
348
  if (artifact.logicalName !== undefined) {
308
349
  assertBoundedString(artifact.logicalName, 'logical name', 256);
309
350
  }
310
- if (
311
- artifact.expectedSize !== undefined &&
312
- (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
313
- ) {
351
+ if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
314
352
  return planError('Firmware update plan artifact size is invalid');
315
353
  }
316
354
  if (
317
- artifact.expectedSha256 !== undefined &&
318
- (typeof artifact.expectedSha256 !== 'string' ||
319
- !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
355
+ typeof artifact.expectedSha256 !== 'string' ||
356
+ !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
320
357
  ) {
321
358
  return planError('Firmware update plan artifact digest is invalid');
322
359
  }
@@ -351,13 +388,11 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
351
388
  plan.targetsToUpdate.some(target =>
352
389
  legacyOnlyTargets.has(target as FirmwareUpdatePlanTarget)
353
390
  )) ||
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.
391
+ // V2 仅在 bootloader 恢复时允许缺少身份;V4 设备在尚未写入序列号时也可升级。
392
+ // 这类 Plan 仍通过设备型号、目标、工件摘要与 preparedPlanDigest 进行约束。
358
393
  (plan.artifacts.length > 0 &&
359
394
  (plan.platform === 'native' || plan.platform === 'desktop') &&
360
- plan.executor !== 'v2' &&
395
+ plan.executor === 'v3' &&
361
396
  plan.deviceIdentity === 'unavailable')
362
397
  ) {
363
398
  return planError('Firmware update plan executor contract is invalid');
@@ -405,7 +440,7 @@ const selectResourceUrl = ({
405
440
 
406
441
  const getExecutor = (features: Features): FirmwareUpdatePlan['executor'] => {
407
442
  const deviceType = getDeviceType(features);
408
- if (deviceType === EDeviceType.Pro2) {
443
+ if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) {
409
444
  return 'v4';
410
445
  }
411
446
  if (
@@ -421,10 +456,10 @@ const buildProtocolV2Artifacts = (
421
456
  release: ReleaseRecord,
422
457
  {
423
458
  includeComponents = true,
424
- includeResources = true,
459
+ componentTargets: selectedComponentTargets,
425
460
  }: {
426
461
  includeComponents?: boolean;
427
- includeResources?: boolean;
462
+ componentTargets?: ReadonlySet<FirmwareUpdatePlanTarget>;
428
463
  } = {}
429
464
  ): {
430
465
  artifacts: FirmwareUpdatePlanArtifact[];
@@ -445,12 +480,21 @@ const buildProtocolV2Artifacts = (
445
480
  includeComponents && components
446
481
  ? [...installOrder, ...Object.keys(components).filter(key => !installOrder.includes(key))]
447
482
  : [];
483
+ const selectedComponentKeys = selectedComponentTargets
484
+ ? componentKeys.filter(key => {
485
+ const component = asRecord(components?.[key]);
486
+ const targetName = asString(component?.target)?.toUpperCase();
487
+ const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
488
+ return !!target && selectedComponentTargets.has(target);
489
+ })
490
+ : componentKeys;
448
491
  const artifacts: FirmwareUpdatePlanArtifact[] = [];
449
492
  const targets: FirmwareUpdatePlanTarget[] = [];
450
- const componentTargets = new Set<FirmwareUpdatePlanTarget>();
451
- for (const key of componentKeys) {
493
+ const componentTargetSet = new Set<FirmwareUpdatePlanTarget>();
494
+ for (const key of selectedComponentKeys) {
452
495
  const component = asRecord(components?.[key]);
453
496
  const targetName = asString(component?.target)?.toUpperCase();
497
+ const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
454
498
  if (targetName === 'ROMLOADER') {
455
499
  throw ERRORS.TypedError(
456
500
  HardwareErrorCode.RuntimeError,
@@ -458,7 +502,6 @@ const buildProtocolV2Artifacts = (
458
502
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
459
503
  );
460
504
  }
461
- const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
462
505
  if (!component || !target) {
463
506
  throw ERRORS.TypedError(
464
507
  HardwareErrorCode.RuntimeError,
@@ -466,14 +509,18 @@ const buildProtocolV2Artifacts = (
466
509
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
467
510
  );
468
511
  }
469
- if (componentTargets.has(target)) {
512
+ if (componentTargetSet.has(target)) {
470
513
  throw ERRORS.TypedError(
471
514
  HardwareErrorCode.RuntimeError,
472
515
  `Protocol V2 component ${key} duplicates target ${target}`,
473
516
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
474
517
  );
475
518
  }
476
- componentTargets.add(target);
519
+ componentTargetSet.add(target);
520
+ const integrity = requireArtifactIntegrity(
521
+ { size: component.expectedSize, sha256: component.fingerprint },
522
+ `Protocol V2 component ${key}`
523
+ );
477
524
  artifacts.push({
478
525
  artifactId: `component:${target}`,
479
526
  role: 'component',
@@ -481,52 +528,12 @@ const buildProtocolV2Artifacts = (
481
528
  url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
482
529
  container: 'raw',
483
530
  logicalName: key,
484
- ...asIntegrity({
485
- size: component.expectedSize,
486
- sha256: component.fingerprint,
487
- }),
531
+ ...integrity,
488
532
  ...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
489
533
  });
490
534
  targets.push(target);
491
535
  }
492
536
 
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
537
  return { artifacts, targets: [...new Set(targets)] };
531
538
  };
532
539
 
@@ -587,6 +594,176 @@ const assertForcedTargetsRepresented = ({
587
594
  }
588
595
  };
589
596
 
597
+ const finalizeFirmwareUpdatePlan = ({
598
+ features,
599
+ firmwareType,
600
+ platform,
601
+ artifacts,
602
+ targetsToUpdate,
603
+ }: {
604
+ features: Features;
605
+ firmwareType: EFirmwareType;
606
+ platform: FirmwareUpdatePlatform;
607
+ artifacts: FirmwareUpdatePlanArtifact[];
608
+ targetsToUpdate: FirmwareUpdatePlanTarget[];
609
+ }): FirmwareUpdatePlan => {
610
+ const executor = getExecutor(features);
611
+ const bootloaderMode = resolveDeviceBootloaderMode(features);
612
+ // 'unavailable' is the reserved degraded-identity sentinel; a device-reported
613
+ // serial must never be able to collide with it.
614
+ const reportedIdentity = getDeviceUUID(features);
615
+ const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
616
+ // Classic 系列仅在 bootloader 恢复时允许缺少身份;部分尚未完成产线写号的
617
+ // Pro2/Neo(V4)在正常模式下也可能没有序列号,因此允许使用降级身份。
618
+ const allowsUnavailableIdentity = (bootloaderMode && executor === 'v2') || executor === 'v4';
619
+ if (
620
+ artifacts.length > 0 &&
621
+ (platform === 'native' || platform === 'desktop') &&
622
+ !deviceIdentity &&
623
+ !allowsUnavailableIdentity
624
+ ) {
625
+ throw ERRORS.TypedError(
626
+ HardwareErrorCode.RuntimeError,
627
+ 'Prepared firmware update requires a stable device identity',
628
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
629
+ );
630
+ }
631
+ const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
632
+ schemaVersion: 2,
633
+ executor,
634
+ deviceIdentity: deviceIdentity || 'unavailable',
635
+ deviceModel: String(getDeviceType(features)),
636
+ firmwareType,
637
+ platform,
638
+ artifacts,
639
+ targetsToUpdate,
640
+ };
641
+ return assertFirmwareUpdatePlan({
642
+ ...planWithoutDigest,
643
+ planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
644
+ });
645
+ };
646
+
647
+ export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
648
+ artifactId: string;
649
+ target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
650
+ container: 'raw' | 'zip';
651
+ logicalName: string;
652
+ expectedSize: number;
653
+ expectedSha256: string;
654
+ targetVersion?: string;
655
+ };
656
+
657
+ export const buildProtocolV2LocalFirmwareUpdatePlan = ({
658
+ features,
659
+ firmwareType,
660
+ platform,
661
+ artifacts,
662
+ }: {
663
+ features: Features;
664
+ firmwareType: EFirmwareType;
665
+ platform: FirmwareUpdatePlatform;
666
+ artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
667
+ }): FirmwareUpdatePlan => {
668
+ if (artifacts.length === 0) {
669
+ return planError('Protocol V2 local firmware plan has no artifacts');
670
+ }
671
+ const plan = finalizeFirmwareUpdatePlan({
672
+ features,
673
+ firmwareType,
674
+ platform,
675
+ artifacts: artifacts.map(artifact => ({
676
+ ...artifact,
677
+ role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
678
+ url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
679
+ })),
680
+ targetsToUpdate: artifacts.map(artifact => artifact.target),
681
+ });
682
+ if (plan.executor !== 'v4') {
683
+ return planError('Protocol V2 local firmware plan requires executor v4');
684
+ }
685
+ return plan;
686
+ };
687
+
688
+ export const buildProtocolV2FirmwareUpdatePlan = ({
689
+ features,
690
+ firmwareType,
691
+ platform,
692
+ release,
693
+ targetsToUpdate,
694
+ forceUpdateTargets,
695
+ resourceArchive,
696
+ }: {
697
+ features: Features;
698
+ firmwareType: EFirmwareType;
699
+ platform: FirmwareUpdatePlatform;
700
+ release: ReleaseRecord | undefined;
701
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
702
+ forceUpdateTargets?: FirmwareUpdatePlanForceTarget[];
703
+ resourceArchive: IProtocolV2ResourceSource | undefined;
704
+ }): FirmwareUpdatePlan => {
705
+ const validatedForceTargets = validateFirmwareUpdatePlanForceTargets(forceUpdateTargets);
706
+ if (validatedForceTargets.some(target => target === 'ble' || target === 'bootloader')) {
707
+ planError('Protocol V2 does not support forced BLE or legacy bootloader targets');
708
+ }
709
+
710
+ const requestedComponentTargets = new Set<FirmwareUpdatePlanTarget>(
711
+ targetsToUpdate.filter(target => target !== 'resource')
712
+ );
713
+ if (
714
+ getDeviceType(features) === EDeviceType.Neo &&
715
+ (requestedComponentTargets.has('se03') || requestedComponentTargets.has('se04'))
716
+ ) {
717
+ planError('Neo does not support Protocol V2 targets se03 or se04');
718
+ }
719
+ const protocolV2 = buildProtocolV2Artifacts(release ?? {}, {
720
+ includeComponents: requestedComponentTargets.size > 0,
721
+ componentTargets: requestedComponentTargets,
722
+ });
723
+ const resourceRequested = targetsToUpdate.includes('resource');
724
+ if (resourceRequested) {
725
+ const archive = resourceArchive;
726
+ if (!archive) {
727
+ return planError('Protocol V2 resource target has no resource archive');
728
+ }
729
+ const integrity = requireArtifactIntegrity(
730
+ { size: archive.archiveSize, sha256: archive.archiveSha256 },
731
+ 'Protocol V2 resource archive'
732
+ );
733
+ protocolV2.artifacts.push({
734
+ artifactId: 'resource:archive',
735
+ role: 'resourceBundle',
736
+ target: 'resource',
737
+ url: assertArtifactUrl(archive.archiveUrl, 'Protocol V2 resource archive'),
738
+ container: 'zip',
739
+ logicalName: 'protocol-v2-resource-archive',
740
+ ...integrity,
741
+ });
742
+ protocolV2.targets.push('resource');
743
+ }
744
+ const representedTargets = new Set(protocolV2.targets);
745
+ const missingTarget = Array.from(requestedComponentTargets).find(
746
+ target => !representedTargets.has(target)
747
+ );
748
+ if (missingTarget) {
749
+ planError(`Protocol V2 release does not contain requested target ${missingTarget}`);
750
+ }
751
+ if (validatedForceTargets.includes('firmware') && protocolV2.targets.length === 0) {
752
+ planError('Forced firmware update target firmware is not represented by the plan');
753
+ }
754
+ if (validatedForceTargets.includes('resource') && !resourceArchive) {
755
+ planError('Forced firmware update target resource has no Protocol V2 resource archive');
756
+ }
757
+
758
+ return finalizeFirmwareUpdatePlan({
759
+ features,
760
+ firmwareType,
761
+ platform,
762
+ artifacts: protocolV2.artifacts,
763
+ targetsToUpdate: protocolV2.targets,
764
+ });
765
+ };
766
+
590
767
  export const buildFirmwareUpdatePlan = ({
591
768
  features,
592
769
  firmwareType,
@@ -623,7 +800,11 @@ export const buildFirmwareUpdatePlan = ({
623
800
  !!asRelease(release);
624
801
  const shouldUpdateFirmware =
625
802
  isUpgrade(firmware) || isRecoveryInstall(firmware) || forcedTargets.has('firmware');
626
- const shouldUpdateResource = isUpgrade(firmware) || forcedTargets.has('resource');
803
+ // V2 cannot write legacy resources once the device is already in bootloader
804
+ // recovery. Do not approve an artifact that its executor cannot apply.
805
+ const shouldUpdateResource =
806
+ !(executor === 'v2' && bootloaderMode) &&
807
+ (isUpgrade(firmware) || forcedTargets.has('resource'));
627
808
 
628
809
  if (
629
810
  executor === 'v4' &&
@@ -635,25 +816,29 @@ export const buildFirmwareUpdatePlan = ({
635
816
  if (executor === 'v4' && (shouldUpdateFirmware || shouldUpdateResource)) {
636
817
  const protocolV2 = buildProtocolV2Artifacts(firmwareRelease ?? {}, {
637
818
  includeComponents: shouldUpdateFirmware,
638
- includeResources: shouldUpdateResource,
639
819
  });
640
820
  artifacts = protocolV2.artifacts;
641
821
  targetsToUpdate = protocolV2.targets;
642
822
  } else {
643
823
  if (isUpgrade(bootloader) || forcedTargets.has('bootloader')) {
824
+ const bootloaderRelease = asRelease(bootloader);
825
+ const integrity = requireArtifactIntegrity(
826
+ {
827
+ size: bootloaderRelease?.bootloaderExpectedSize,
828
+ sha256: bootloaderRelease?.bootloaderFingerprint,
829
+ },
830
+ 'Bootloader release'
831
+ );
644
832
  artifacts.push({
645
833
  artifactId: 'bootloader',
646
834
  role: 'bootloader',
647
835
  target: 'bootloader',
648
- url: assertArtifactUrl(asRelease(bootloader)?.bootloaderResource, 'Bootloader release'),
836
+ url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
649
837
  container: 'raw',
650
- ...asIntegrity({
651
- size: asRelease(bootloader)?.bootloaderExpectedSize,
652
- sha256: asRelease(bootloader)?.bootloaderFingerprint,
653
- }),
654
- ...(asVersion(asRelease(bootloader)?.bootloaderVersion)
838
+ ...integrity,
839
+ ...(asVersion(bootloaderRelease?.bootloaderVersion)
655
840
  ? {
656
- targetVersion: asVersion(asRelease(bootloader)?.bootloaderVersion),
841
+ targetVersion: asVersion(bootloaderRelease?.bootloaderVersion),
657
842
  }
658
843
  : {}),
659
844
  });
@@ -665,16 +850,17 @@ export const buildFirmwareUpdatePlan = ({
665
850
  });
666
851
  }
667
852
  if (shouldUpdateFirmware) {
853
+ const integrity = requireArtifactIntegrity(
854
+ { size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
855
+ 'Firmware release'
856
+ );
668
857
  artifacts.push({
669
858
  artifactId: 'firmware',
670
859
  role: 'firmware',
671
860
  target: 'firmware',
672
861
  url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
673
862
  container: 'raw',
674
- ...asIntegrity({
675
- size: firmwareRelease.expectedSize,
676
- sha256: firmwareRelease.fingerprint,
677
- }),
863
+ ...integrity,
678
864
  ...(asVersion(firmwareRelease.version)
679
865
  ? { targetVersion: asVersion(firmwareRelease.version) }
680
866
  : {}),
@@ -687,13 +873,8 @@ export const buildFirmwareUpdatePlan = ({
687
873
  platform,
688
874
  });
689
875
  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({
876
+ const integrity = requireArtifactIntegrity(
877
+ {
697
878
  size:
698
879
  resourceUrl === asString(firmwareRelease.fullResource)
699
880
  ? firmwareRelease.fullResourceExpectedSize
@@ -702,23 +883,36 @@ export const buildFirmwareUpdatePlan = ({
702
883
  resourceUrl === asString(firmwareRelease.fullResource)
703
884
  ? firmwareRelease.fullResourceFingerprint
704
885
  : firmwareRelease.resourceFingerprint,
705
- }),
886
+ },
887
+ 'Legacy resource archive'
888
+ );
889
+ artifacts.push({
890
+ artifactId: 'resource',
891
+ role: 'resource',
892
+ target: 'resource',
893
+ url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
894
+ container: 'zip',
895
+ ...integrity,
706
896
  });
707
897
  }
708
898
  }
709
899
  }
710
900
  if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
711
901
  const bleRelease = asRelease(ble);
902
+ const integrity = requireArtifactIntegrity(
903
+ {
904
+ size: bleRelease?.expectedSize,
905
+ sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
906
+ },
907
+ 'BLE release'
908
+ );
712
909
  artifacts.push({
713
910
  artifactId: 'ble',
714
911
  role: 'ble',
715
912
  target: 'ble',
716
913
  url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
717
914
  container: 'raw',
718
- ...asIntegrity({
719
- size: bleRelease?.expectedSize,
720
- sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
721
- }),
915
+ ...integrity,
722
916
  ...(asVersion(bleRelease?.version)
723
917
  ? { targetVersion: asVersion(bleRelease?.version) }
724
918
  : {}),
@@ -727,46 +921,25 @@ export const buildFirmwareUpdatePlan = ({
727
921
  targetsToUpdate = [...new Set(artifacts.map(artifact => artifact.target))];
728
922
  }
729
923
 
924
+ if (
925
+ executor !== 'v4' &&
926
+ targetsToUpdate.includes('resource') &&
927
+ !targetsToUpdate.includes('firmware')
928
+ ) {
929
+ planError('Legacy resource updates require a firmware target');
930
+ }
931
+
730
932
  assertForcedTargetsRepresented({
731
933
  executor,
732
934
  forcedTargets: validatedForceTargets,
733
935
  artifacts,
734
936
  targetsToUpdate,
735
937
  });
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)),
938
+ return finalizeFirmwareUpdatePlan({
939
+ features,
763
940
  firmwareType,
764
941
  platform,
765
942
  artifacts,
766
943
  targetsToUpdate,
767
- };
768
- return assertFirmwareUpdatePlan({
769
- ...planWithoutDigest,
770
- planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
771
944
  });
772
945
  };