@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.
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +112 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
- package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
- package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
- package/__tests__/protocol-v2-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1236 -516
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts +2 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/CheckFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +1 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +8 -8
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +0 -1
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts +33 -0
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +108 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1633 -967
- package/dist/protocols/protocol-v2/resources.d.ts +10 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
- package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +4 -12
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/settings.d.ts +35 -36
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +108 -80
- package/src/api/CheckBLEFirmwareRelease.ts +37 -5
- package/src/api/CheckBootloaderRelease.ts +35 -3
- package/src/api/CheckFirmwareRelease.ts +35 -7
- package/src/api/FirmwareUpdateV2.ts +108 -73
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +885 -403
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +16 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
- package/src/api/firmware/protocolV2Release.ts +168 -0
- package/src/data-manager/DataManager.ts +35 -19
- package/src/index.ts +10 -0
- package/src/protocols/protocol-v2/resources.ts +157 -341
- package/src/types/api/checkAllFirmwareRelease.ts +27 -12
- package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
- package/src/types/api/checkBootloaderRelease.ts +2 -6
- package/src/types/api/checkFirmwareRelease.ts +2 -13
- package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/export.ts +6 -1
- package/src/types/api/firmwareUpdate.ts +12 -17
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/settings.ts +35 -61
- 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', '
|
|
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 !==
|
|
318
|
-
(
|
|
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
|
-
//
|
|
355
|
-
//
|
|
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
|
|
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
|
-
|
|
454
|
+
componentTargets: selectedComponentTargets,
|
|
425
455
|
}: {
|
|
426
456
|
includeComponents?: boolean;
|
|
427
|
-
|
|
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
|
|
451
|
-
for (const key of
|
|
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 (
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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(
|
|
831
|
+
url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
|
|
649
832
|
container: 'raw',
|
|
650
|
-
...
|
|
651
|
-
|
|
652
|
-
sha256: asRelease(bootloader)?.bootloaderFingerprint,
|
|
653
|
-
}),
|
|
654
|
-
...(asVersion(asRelease(bootloader)?.bootloaderVersion)
|
|
833
|
+
...integrity,
|
|
834
|
+
...(asVersion(bootloaderRelease?.bootloaderVersion)
|
|
655
835
|
? {
|
|
656
|
-
targetVersion: asVersion(
|
|
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
|
-
...
|
|
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
|
-
|
|
691
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|