@onekeyfe/hd-core 1.2.0-alpha.96 → 1.2.0-alpha.97

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 (89) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +84 -404
  2. package/__tests__/firmware-update/device-update-bootloader.test.ts +2 -6
  3. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -9
  4. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
  5. package/__tests__/firmware-update/firmware-update-plan.test.ts +70 -131
  6. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +1 -53
  7. package/__tests__/protocol-v2-resources.test.ts +232 -148
  8. package/__tests__/protocol-v2.test.ts +517 -1106
  9. package/dist/api/CheckAllFirmwareRelease.d.ts +3 -2
  10. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  11. package/dist/api/CheckBLEFirmwareRelease.d.ts +2 -3
  12. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  13. package/dist/api/CheckBootloaderRelease.d.ts +1 -2
  14. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  15. package/dist/api/CheckFirmwareRelease.d.ts +2 -3
  16. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  17. package/dist/api/FirmwareUpdateV2.d.ts +0 -1
  18. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  20. package/dist/api/FirmwareUpdateV4.d.ts +8 -6
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  23. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  24. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  25. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  26. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +1 -48
  28. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  30. package/dist/api/firmware/getBinary.d.ts +1 -0
  31. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  32. package/dist/data-manager/DataManager.d.ts +3 -4
  33. package/dist/data-manager/DataManager.d.ts.map +1 -1
  34. package/dist/index.d.ts +111 -103
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +849 -1313
  37. package/dist/protocols/protocol-v2/resources.d.ts +28 -10
  38. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  39. package/dist/types/api/checkAllFirmwareRelease.d.ts +8 -20
  40. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/types/api/checkBLEFirmwareRelease.d.ts +13 -2
  42. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  43. package/dist/types/api/checkBootloaderRelease.d.ts +6 -2
  44. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  45. package/dist/types/api/checkFirmwareRelease.d.ts +13 -2
  46. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  47. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  48. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  49. package/dist/types/api/export.d.ts +1 -1
  50. package/dist/types/api/export.d.ts.map +1 -1
  51. package/dist/types/api/firmwareUpdate.d.ts +11 -3
  52. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  53. package/dist/types/settings.d.ts +36 -35
  54. package/dist/types/settings.d.ts.map +1 -1
  55. package/package.json +4 -4
  56. package/src/api/CheckAllFirmwareRelease.ts +80 -108
  57. package/src/api/CheckBLEFirmwareRelease.ts +5 -37
  58. package/src/api/CheckBootloaderRelease.ts +3 -35
  59. package/src/api/CheckFirmwareRelease.ts +7 -35
  60. package/src/api/FirmwareUpdateV2.ts +3 -15
  61. package/src/api/FirmwareUpdateV3.ts +7 -29
  62. package/src/api/FirmwareUpdateV4.ts +407 -824
  63. package/src/api/UploadPortfolio.ts +0 -18
  64. package/src/api/device/DeviceUpdateBootloader.ts +5 -26
  65. package/src/api/firmware/FirmwareHostBinding.ts +3 -16
  66. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
  67. package/src/api/firmware/FirmwareUpdatePlan.ts +88 -226
  68. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +3 -10
  69. package/src/data-manager/DataManager.ts +19 -35
  70. package/src/index.ts +0 -10
  71. package/src/protocols/protocol-v2/resources.ts +341 -157
  72. package/src/types/api/checkAllFirmwareRelease.ts +12 -27
  73. package/src/types/api/checkBLEFirmwareRelease.ts +13 -4
  74. package/src/types/api/checkBootloaderRelease.ts +6 -2
  75. package/src/types/api/checkFirmwareRelease.ts +13 -2
  76. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  77. package/src/types/api/export.ts +1 -6
  78. package/src/types/api/firmwareUpdate.ts +16 -10
  79. package/src/types/settings.ts +61 -35
  80. package/src/utils/deviceFeaturesUtils.ts +2 -2
  81. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -199
  82. package/__tests__/firmware-memory-host.test.ts +0 -112
  83. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +0 -469
  84. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  85. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  86. package/dist/api/firmware/protocolV2Release.d.ts +0 -33
  87. package/dist/api/firmware/protocolV2Release.d.ts.map +0 -1
  88. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
  89. package/src/api/firmware/protocolV2Release.ts +0 -168
@@ -1,6 +1,3 @@
1
- import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
2
-
3
- import { supportsProtocolV2Message } from '../protocols/protocol-v2/features';
4
1
  import FileWrite from './FileWrite';
5
2
 
6
3
  export type UploadPortfolioParams = {
@@ -10,8 +7,6 @@ export type UploadPortfolioParams = {
10
7
 
11
8
  const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.okpkg.pending';
12
9
  const PORTFOLIO_CHUNK_SIZE = 2048;
13
- const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
14
- const PORTFOLIO_UPDATE_MESSAGE_TYPE = 61400;
15
10
 
16
11
  export default class UploadPortfolio extends FileWrite {
17
12
  init() {
@@ -34,19 +29,6 @@ export default class UploadPortfolio extends FileWrite {
34
29
  }
35
30
 
36
31
  async run() {
37
- const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
38
- const hasFileWrite = supportsProtocolV2Message(
39
- protocolInfo,
40
- FILESYSTEM_FILE_WRITE_MESSAGE_TYPE
41
- );
42
- const hasPortfolioUpdate = supportsProtocolV2Message(
43
- protocolInfo,
44
- PORTFOLIO_UPDATE_MESSAGE_TYPE
45
- );
46
- if (!hasFileWrite || !hasPortfolioUpdate) {
47
- throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
48
- }
49
-
50
32
  const stagedFile = await super.run();
51
33
  this.throwIfAborted();
52
34
  await this.device.commands.typedCall('PortfolioUpdate', 'Success', {});
@@ -14,7 +14,6 @@ import { resolveFirmwareUpdateHostBinding } from '../firmware/FirmwareHostBindin
14
14
  import {
15
15
  assertFirmwareUpdatePreparedPlanBinding,
16
16
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
17
- validateFirmwareUpdatePreparedPlan,
18
17
  } from '../firmware/FirmwareUpdatePreparedPlan';
19
18
  import { getDeviceType, getDeviceUUID } from '../../utils';
20
19
  import { resolveDeviceBootloaderMode } from '../../utils/deviceFeaturesCompat';
@@ -158,42 +157,22 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
158
157
  const { features } = device;
159
158
 
160
159
  const payload = this.payload as DeviceUpdateBootloaderParams;
161
- const hasPreparedPlan = payload.preparedPlan !== undefined;
162
- const hasPreparedArtifact = payload.artifact !== undefined;
163
- if (hasPreparedPlan !== hasPreparedArtifact) {
164
- throw ERRORS.TypedError(
165
- HardwareErrorCode.CallMethodInvalidParameter,
166
- 'Prepared bootloader plans require exactly one prepared artifact'
167
- );
168
- }
169
- if (payload.binary !== undefined && hasPreparedArtifact) {
170
- throw ERRORS.TypedError(
171
- HardwareErrorCode.CallMethodInvalidParameter,
172
- 'Bootloader binary and prepared artifact are mutually exclusive'
173
- );
174
- }
175
- const preparedPlan = hasPreparedArtifact
176
- ? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
160
+ const hostBinding = payload.artifact
161
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
177
162
  : undefined;
178
- const artifactReader = preparedPlan
179
- ? resolveFirmwareUpdateHostBinding(
180
- payload.hostBindingGeneration,
181
- preparedPlan.preparedPlanDigest
182
- ).artifactReader
183
- : payload.artifactReader;
184
163
  const executionParams = {
185
164
  ...payload,
186
- artifactReader,
165
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
187
166
  };
188
167
  if (payload.artifact) {
189
168
  assertFirmwareUpdatePreparedPlanDeviceIdentity({
190
- preparedPlan,
169
+ preparedPlan: payload.preparedPlan,
191
170
  deviceIdentity: getDeviceUUID(features) || undefined,
192
171
  bootloaderMode: resolveDeviceBootloaderMode(features),
193
172
  deviceModel: String(getDeviceType(features)),
194
173
  });
195
174
  assertFirmwareUpdatePreparedPlanBinding({
196
- preparedPlan,
175
+ preparedPlan: payload.preparedPlan,
197
176
  executor: 'v2',
198
177
  scopeTargets: ['bootloader'],
199
178
  bindings: [
@@ -16,8 +16,6 @@ class FirmwareHostBindingRegistry {
16
16
  register(binding: FirmwareUpdateHostBinding): number {
17
17
  if (
18
18
  !binding ||
19
- typeof binding.preparedPlanDigest !== 'string' ||
20
- !/^[0-9a-f]{64}$/i.test(binding.preparedPlanDigest) ||
21
19
  typeof binding.artifactReader?.open !== 'function' ||
22
20
  typeof binding.artifactReader.read !== 'function' ||
23
21
  typeof binding.artifactReader.close !== 'function'
@@ -42,7 +40,7 @@ class FirmwareHostBindingRegistry {
42
40
  return this.generation;
43
41
  }
44
42
 
45
- resolve(generation: number, preparedPlanDigest?: string): FirmwareUpdateHostBinding {
43
+ resolve(generation: number): FirmwareUpdateHostBinding {
46
44
  if (
47
45
  !Number.isSafeInteger(generation) ||
48
46
  generation <= 0 ||
@@ -52,21 +50,12 @@ class FirmwareHostBindingRegistry {
52
50
  return bindingError(`Firmware host binding generation ${generation} is stale`);
53
51
  }
54
52
  const { binding } = this;
55
- if (
56
- preparedPlanDigest !== undefined &&
57
- binding.preparedPlanDigest.toLowerCase() !== preparedPlanDigest.toLowerCase()
58
- ) {
59
- return bindingError(
60
- `Firmware host binding generation ${generation} does not match the prepared plan`
61
- );
62
- }
63
53
  const assertCurrent = () => {
64
54
  if (generation !== this.generation || binding !== this.binding) {
65
55
  bindingError(`Firmware host binding generation ${generation} is stale`);
66
56
  }
67
57
  };
68
58
  return {
69
- preparedPlanDigest: binding.preparedPlanDigest,
70
59
  artifactReader: {
71
60
  open: async input => {
72
61
  assertCurrent();
@@ -107,7 +96,5 @@ export const getFirmwareUpdateHostBindingGeneration = (): number =>
107
96
  firmwareHostBindingRegistry.getGeneration();
108
97
 
109
98
  export const resolveFirmwareUpdateHostBinding = (
110
- generation: number | undefined,
111
- preparedPlanDigest?: string
112
- ): FirmwareUpdateHostBinding =>
113
- firmwareHostBindingRegistry.resolve(generation ?? Number.NaN, preparedPlanDigest);
99
+ generation: number | undefined
100
+ ): FirmwareUpdateHostBinding => firmwareHostBindingRegistry.resolve(generation ?? Number.NaN);
@@ -191,6 +191,8 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
191
191
  !hasPromptedWebDevice &&
192
192
  !isPromptingWebDevice
193
193
  ) {
194
+ clearInterval(intervalTimer);
195
+ clearTimeout(timeoutTimer);
194
196
  isPromptingWebDevice = true;
195
197
  try {
196
198
  this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
@@ -10,14 +10,13 @@ import {
10
10
  getDeviceFirmwareVersion,
11
11
  } from '../../utils/deviceVersionUtils';
12
12
 
13
- import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
14
13
  import type {
15
14
  FirmwareUpdatePlan,
16
15
  FirmwareUpdatePlanArtifact,
17
16
  FirmwareUpdatePlanForceTarget,
18
17
  FirmwareUpdatePlanTarget,
19
18
  } from '../../types/api/firmwareUpdatePlan';
20
- import type { Features, IProtocolV2ResourceSource } from '../../types';
19
+ import type { Features } from '../../types';
21
20
 
22
21
  type FirmwareUpdatePlatform = 'native' | 'desktop' | 'ext' | 'web' | 'web-embed';
23
22
 
@@ -31,6 +30,7 @@ type ReleaseRecord = {
31
30
  version?: unknown;
32
31
  components?: unknown;
33
32
  installOrder?: unknown;
33
+ resourceBundles?: unknown;
34
34
  fingerprint?: unknown;
35
35
  fingerprintWeb?: unknown;
36
36
  expectedSize?: unknown;
@@ -62,11 +62,6 @@ 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
-
70
65
  const asRecord = (value: unknown): Record<string, unknown> | undefined =>
71
66
  value && typeof value === 'object' && !Array.isArray(value)
72
67
  ? (value as Record<string, unknown>)
@@ -196,23 +191,6 @@ export const validateFirmwareUpdatePlanForceTargets = (
196
191
  return [...value] as FirmwareUpdatePlanForceTarget[];
197
192
  };
198
193
 
199
- export const validateProtocolV2FirmwareUpdateTargets = (
200
- value: unknown
201
- ): FirmwareUpdateV4Target[] => {
202
- if (value === undefined) {
203
- return [];
204
- }
205
- if (
206
- !Array.isArray(value) ||
207
- value.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
208
- value.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
209
- new Set(value).size !== value.length
210
- ) {
211
- return planError('Protocol V2 firmware update targets are invalid');
212
- }
213
- return [...value] as FirmwareUpdateV4Target[];
214
- };
215
-
216
194
  const assertExactKeys = (
217
195
  value: Record<string, unknown>,
218
196
  required: readonly string[],
@@ -373,11 +351,13 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
373
351
  plan.targetsToUpdate.some(target =>
374
352
  legacyOnlyTargets.has(target as FirmwareUpdatePlanTarget)
375
353
  )) ||
376
- // V2 仅在 bootloader 恢复时允许缺少身份;V4 设备在尚未写入序列号时也可升级。
377
- // 这类 Plan 仍通过设备型号、目标、工件摘要与 preparedPlanDigest 进行约束。
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.
378
358
  (plan.artifacts.length > 0 &&
379
359
  (plan.platform === 'native' || plan.platform === 'desktop') &&
380
- plan.executor === 'v3' &&
360
+ plan.executor !== 'v2' &&
381
361
  plan.deviceIdentity === 'unavailable')
382
362
  ) {
383
363
  return planError('Firmware update plan executor contract is invalid');
@@ -425,7 +405,7 @@ const selectResourceUrl = ({
425
405
 
426
406
  const getExecutor = (features: Features): FirmwareUpdatePlan['executor'] => {
427
407
  const deviceType = getDeviceType(features);
428
- if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) {
408
+ if (deviceType === EDeviceType.Pro2) {
429
409
  return 'v4';
430
410
  }
431
411
  if (
@@ -441,10 +421,10 @@ const buildProtocolV2Artifacts = (
441
421
  release: ReleaseRecord,
442
422
  {
443
423
  includeComponents = true,
444
- componentTargets: selectedComponentTargets,
424
+ includeResources = true,
445
425
  }: {
446
426
  includeComponents?: boolean;
447
- componentTargets?: ReadonlySet<FirmwareUpdatePlanTarget>;
427
+ includeResources?: boolean;
448
428
  } = {}
449
429
  ): {
450
430
  artifacts: FirmwareUpdatePlanArtifact[];
@@ -465,21 +445,12 @@ const buildProtocolV2Artifacts = (
465
445
  includeComponents && components
466
446
  ? [...installOrder, ...Object.keys(components).filter(key => !installOrder.includes(key))]
467
447
  : [];
468
- const selectedComponentKeys = selectedComponentTargets
469
- ? componentKeys.filter(key => {
470
- const component = asRecord(components?.[key]);
471
- const targetName = asString(component?.target)?.toUpperCase();
472
- const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
473
- return !!target && selectedComponentTargets.has(target);
474
- })
475
- : componentKeys;
476
448
  const artifacts: FirmwareUpdatePlanArtifact[] = [];
477
449
  const targets: FirmwareUpdatePlanTarget[] = [];
478
- const componentTargetSet = new Set<FirmwareUpdatePlanTarget>();
479
- for (const key of selectedComponentKeys) {
450
+ const componentTargets = new Set<FirmwareUpdatePlanTarget>();
451
+ for (const key of componentKeys) {
480
452
  const component = asRecord(components?.[key]);
481
453
  const targetName = asString(component?.target)?.toUpperCase();
482
- const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
483
454
  if (targetName === 'ROMLOADER') {
484
455
  throw ERRORS.TypedError(
485
456
  HardwareErrorCode.RuntimeError,
@@ -487,6 +458,7 @@ const buildProtocolV2Artifacts = (
487
458
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
488
459
  );
489
460
  }
461
+ const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
490
462
  if (!component || !target) {
491
463
  throw ERRORS.TypedError(
492
464
  HardwareErrorCode.RuntimeError,
@@ -494,21 +466,14 @@ const buildProtocolV2Artifacts = (
494
466
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
495
467
  );
496
468
  }
497
- if (componentTargetSet.has(target)) {
469
+ if (componentTargets.has(target)) {
498
470
  throw ERRORS.TypedError(
499
471
  HardwareErrorCode.RuntimeError,
500
472
  `Protocol V2 component ${key} duplicates target ${target}`,
501
473
  { firmwareUpdateCode: 'FirmwarePlanInvalid' }
502
474
  );
503
475
  }
504
- componentTargetSet.add(target);
505
- const integrity = asIntegrity({
506
- size: component.expectedSize,
507
- sha256: component.fingerprint,
508
- });
509
- if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
510
- planError(`Protocol V2 component ${key} integrity metadata is invalid`);
511
- }
476
+ componentTargets.add(target);
512
477
  artifacts.push({
513
478
  artifactId: `component:${target}`,
514
479
  role: 'component',
@@ -516,12 +481,52 @@ const buildProtocolV2Artifacts = (
516
481
  url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
517
482
  container: 'raw',
518
483
  logicalName: key,
519
- ...integrity,
484
+ ...asIntegrity({
485
+ size: component.expectedSize,
486
+ sha256: component.fingerprint,
487
+ }),
520
488
  ...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
521
489
  });
522
490
  targets.push(target);
523
491
  }
524
492
 
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
+ }
525
530
  return { artifacts, targets: [...new Set(targets)] };
526
531
  };
527
532
 
@@ -582,179 +587,6 @@ const assertForcedTargetsRepresented = ({
582
587
  }
583
588
  };
584
589
 
585
- const finalizeFirmwareUpdatePlan = ({
586
- features,
587
- firmwareType,
588
- platform,
589
- artifacts,
590
- targetsToUpdate,
591
- }: {
592
- features: Features;
593
- firmwareType: EFirmwareType;
594
- platform: FirmwareUpdatePlatform;
595
- artifacts: FirmwareUpdatePlanArtifact[];
596
- targetsToUpdate: FirmwareUpdatePlanTarget[];
597
- }): FirmwareUpdatePlan => {
598
- const executor = getExecutor(features);
599
- const bootloaderMode = resolveDeviceBootloaderMode(features);
600
- // 'unavailable' is the reserved degraded-identity sentinel; a device-reported
601
- // serial must never be able to collide with it.
602
- const reportedIdentity = getDeviceUUID(features);
603
- const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
604
- // Classic 系列仅在 bootloader 恢复时允许缺少身份;部分尚未完成产线写号的
605
- // Pro2/Neo(V4)在正常模式下也可能没有序列号,因此允许使用降级身份。
606
- const allowsUnavailableIdentity = (bootloaderMode && executor === 'v2') || executor === 'v4';
607
- if (
608
- artifacts.length > 0 &&
609
- (platform === 'native' || platform === 'desktop') &&
610
- !deviceIdentity &&
611
- !allowsUnavailableIdentity
612
- ) {
613
- throw ERRORS.TypedError(
614
- HardwareErrorCode.RuntimeError,
615
- 'Prepared firmware update requires a stable device identity',
616
- { firmwareUpdateCode: 'FirmwarePlanInvalid' }
617
- );
618
- }
619
- const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
620
- schemaVersion: 2,
621
- executor,
622
- deviceIdentity: deviceIdentity || 'unavailable',
623
- deviceModel: String(getDeviceType(features)),
624
- firmwareType,
625
- platform,
626
- artifacts,
627
- targetsToUpdate,
628
- };
629
- return assertFirmwareUpdatePlan({
630
- ...planWithoutDigest,
631
- planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
632
- });
633
- };
634
-
635
- export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
636
- artifactId: string;
637
- target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
638
- container: 'raw' | 'zip';
639
- logicalName: string;
640
- expectedSize: number;
641
- expectedSha256: string;
642
- targetVersion?: string;
643
- };
644
-
645
- export const buildProtocolV2LocalFirmwareUpdatePlan = ({
646
- features,
647
- firmwareType,
648
- platform,
649
- artifacts,
650
- }: {
651
- features: Features;
652
- firmwareType: EFirmwareType;
653
- platform: FirmwareUpdatePlatform;
654
- artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
655
- }): FirmwareUpdatePlan => {
656
- if (artifacts.length === 0) {
657
- return planError('Protocol V2 local firmware plan has no artifacts');
658
- }
659
- const plan = finalizeFirmwareUpdatePlan({
660
- features,
661
- firmwareType,
662
- platform,
663
- artifacts: artifacts.map(artifact => ({
664
- ...artifact,
665
- role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
666
- url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
667
- })),
668
- targetsToUpdate: artifacts.map(artifact => artifact.target),
669
- });
670
- if (plan.executor !== 'v4') {
671
- return planError('Protocol V2 local firmware plan requires executor v4');
672
- }
673
- return plan;
674
- };
675
-
676
- export const buildProtocolV2FirmwareUpdatePlan = ({
677
- features,
678
- firmwareType,
679
- platform,
680
- release,
681
- targetsToUpdate,
682
- forceUpdateTargets,
683
- resourceArchive,
684
- }: {
685
- features: Features;
686
- firmwareType: EFirmwareType;
687
- platform: FirmwareUpdatePlatform;
688
- release: ReleaseRecord | undefined;
689
- targetsToUpdate: readonly FirmwareUpdateV4Target[];
690
- forceUpdateTargets?: FirmwareUpdatePlanForceTarget[];
691
- resourceArchive: IProtocolV2ResourceSource | undefined;
692
- }): FirmwareUpdatePlan => {
693
- const validatedForceTargets = validateFirmwareUpdatePlanForceTargets(forceUpdateTargets);
694
- if (validatedForceTargets.some(target => target === 'ble' || target === 'bootloader')) {
695
- planError('Protocol V2 does not support forced BLE or legacy bootloader targets');
696
- }
697
-
698
- const requestedComponentTargets = new Set<FirmwareUpdatePlanTarget>(
699
- targetsToUpdate.filter(target => target !== 'resource')
700
- );
701
- if (
702
- getDeviceType(features) === EDeviceType.Neo &&
703
- (requestedComponentTargets.has('se03') || requestedComponentTargets.has('se04'))
704
- ) {
705
- planError('Neo does not support Protocol V2 targets se03 or se04');
706
- }
707
- const protocolV2 = buildProtocolV2Artifacts(release ?? {}, {
708
- includeComponents: requestedComponentTargets.size > 0,
709
- componentTargets: requestedComponentTargets,
710
- });
711
- const resourceRequested = targetsToUpdate.includes('resource');
712
- if (resourceRequested) {
713
- const archive = resourceArchive;
714
- if (!archive) {
715
- return planError('Protocol V2 resource target has no resource archive');
716
- }
717
- const integrity = asIntegrity({
718
- size: archive.archiveSize,
719
- sha256: archive.archiveSha256,
720
- });
721
- if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
722
- planError('Protocol V2 resource archive integrity metadata is invalid');
723
- }
724
- protocolV2.artifacts.push({
725
- artifactId: 'resource:archive',
726
- role: 'resourceBundle',
727
- target: 'resource',
728
- url: assertArtifactUrl(archive.archiveUrl, 'Protocol V2 resource archive'),
729
- container: 'zip',
730
- logicalName: 'protocol-v2-resource-archive',
731
- ...integrity,
732
- });
733
- protocolV2.targets.push('resource');
734
- }
735
- const representedTargets = new Set(protocolV2.targets);
736
- const missingTarget = Array.from(requestedComponentTargets).find(
737
- target => !representedTargets.has(target)
738
- );
739
- if (missingTarget) {
740
- planError(`Protocol V2 release does not contain requested target ${missingTarget}`);
741
- }
742
- if (validatedForceTargets.includes('firmware') && protocolV2.targets.length === 0) {
743
- planError('Forced firmware update target firmware is not represented by the plan');
744
- }
745
- if (validatedForceTargets.includes('resource') && !resourceArchive) {
746
- planError('Forced firmware update target resource has no Protocol V2 resource archive');
747
- }
748
-
749
- return finalizeFirmwareUpdatePlan({
750
- features,
751
- firmwareType,
752
- platform,
753
- artifacts: protocolV2.artifacts,
754
- targetsToUpdate: protocolV2.targets,
755
- });
756
- };
757
-
758
590
  export const buildFirmwareUpdatePlan = ({
759
591
  features,
760
592
  firmwareType,
@@ -803,6 +635,7 @@ export const buildFirmwareUpdatePlan = ({
803
635
  if (executor === 'v4' && (shouldUpdateFirmware || shouldUpdateResource)) {
804
636
  const protocolV2 = buildProtocolV2Artifacts(firmwareRelease ?? {}, {
805
637
  includeComponents: shouldUpdateFirmware,
638
+ includeResources: shouldUpdateResource,
806
639
  });
807
640
  artifacts = protocolV2.artifacts;
808
641
  targetsToUpdate = protocolV2.targets;
@@ -900,11 +733,40 @@ export const buildFirmwareUpdatePlan = ({
900
733
  artifacts,
901
734
  targetsToUpdate,
902
735
  });
903
- return finalizeFirmwareUpdatePlan({
904
- features,
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)),
905
763
  firmwareType,
906
764
  platform,
907
765
  artifacts,
908
766
  targetsToUpdate,
767
+ };
768
+ return assertFirmwareUpdatePlan({
769
+ ...planWithoutDigest,
770
+ planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
909
771
  });
910
772
  };
@@ -278,14 +278,7 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
278
278
  artifactIds.add(artifact.artifactId);
279
279
  artifactTargets.add(artifact.target);
280
280
  assertFirmwareArtifactReference(artifact.artifact);
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
- }
281
+ artifact.materializedEntries?.forEach(assertPreparedEntry);
289
282
  }
290
283
  if (
291
284
  preparedPlan.targetsToUpdate.some(target => !FIRMWARE_UPDATE_PLAN_TARGETS.has(target)) ||
@@ -349,8 +342,8 @@ export const assertFirmwareUpdatePreparedPlanDeviceIdentity = ({
349
342
  }
350
343
  const pinnedIdentity = degradedPlanIdentityPins.get(preparedPlan.leaseRef);
351
344
  if (!deviceIdentity) {
352
- // V4 设备可能尚未写入序列号;V2 则只允许 bootloader 恢复流程缺少身份。
353
- if (preparedPlan.executor !== 'v4' && bootloaderMode !== true) {
345
+ // Still identity-less: only legitimate while the device sits in bootloader.
346
+ if (bootloaderMode !== true) {
354
347
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
355
348
  }
356
349
  return preparedPlan;