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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/__tests__/firmware-update/device-update-bootloader.test.ts +8 -1
  2. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +106 -2
  3. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  4. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
  5. package/__tests__/firmware-update/firmware-update-plan.test.ts +101 -0
  6. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  7. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  8. package/__tests__/protocol-v2.test.ts +131 -0
  9. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  14. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  15. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  18. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  19. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -3
  21. package/dist/index.js +359 -157
  22. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  23. package/dist/types/api/firmwareUpdate.d.ts +1 -1
  24. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  25. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  26. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/api/FirmwareUpdateV2.ts +95 -72
  29. package/src/api/FirmwareUpdateV3.ts +74 -51
  30. package/src/api/FirmwareUpdateV4.ts +72 -7
  31. package/src/api/device/DeviceUpdateBootloader.ts +17 -12
  32. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  33. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
  34. package/src/api/firmware/FirmwareUpdatePlan.ts +70 -48
  35. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +17 -0
  36. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  37. package/src/types/api/firmwareUpdate.ts +2 -1
  38. package/src/types/api/firmwareUpdatePlan.ts +2 -2
@@ -14,6 +14,7 @@ import { resolveFirmwareUpdateHostBinding } from '../firmware/FirmwareHostBindin
14
14
  import {
15
15
  assertFirmwareUpdatePreparedPlanBinding,
16
16
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
17
+ getFirmwareUpdatePreparedRawArtifact,
17
18
  validateFirmwareUpdatePreparedPlan,
18
19
  } from '../firmware/FirmwareUpdatePreparedPlan';
19
20
  import { getDeviceType, getDeviceUUID } from '../../utils';
@@ -159,22 +160,28 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
159
160
 
160
161
  const payload = this.payload as DeviceUpdateBootloaderParams;
161
162
  const hasPreparedPlan = payload.preparedPlan !== undefined;
162
- const hasPreparedArtifact = payload.artifact !== undefined;
163
- if (hasPreparedPlan !== hasPreparedArtifact) {
163
+ if (hasPreparedPlan && payload.binary !== undefined) {
164
164
  throw ERRORS.TypedError(
165
165
  HardwareErrorCode.CallMethodInvalidParameter,
166
- 'Prepared bootloader plans require exactly one prepared artifact'
166
+ 'Prepared bootloader plans cannot be combined with a legacy binary'
167
167
  );
168
168
  }
169
- if (payload.binary !== undefined && hasPreparedArtifact) {
169
+ if (!hasPreparedPlan && payload.artifact !== undefined) {
170
170
  throw ERRORS.TypedError(
171
171
  HardwareErrorCode.CallMethodInvalidParameter,
172
- 'Bootloader binary and prepared artifact are mutually exclusive'
172
+ 'Bootloader artifacts require a prepared plan'
173
173
  );
174
174
  }
175
- const preparedPlan = hasPreparedArtifact
175
+ const preparedPlan = hasPreparedPlan
176
176
  ? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
177
177
  : undefined;
178
+ const plannedArtifact = preparedPlan
179
+ ? getFirmwareUpdatePreparedRawArtifact({
180
+ preparedPlan,
181
+ target: 'bootloader',
182
+ role: 'bootloader',
183
+ }).artifact
184
+ : undefined;
178
185
  const artifactReader = preparedPlan
179
186
  ? resolveFirmwareUpdateHostBinding(
180
187
  payload.hostBindingGeneration,
@@ -185,7 +192,7 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
185
192
  ...payload,
186
193
  artifactReader,
187
194
  };
188
- if (payload.artifact) {
195
+ if (preparedPlan && plannedArtifact) {
189
196
  assertFirmwareUpdatePreparedPlanDeviceIdentity({
190
197
  preparedPlan,
191
198
  deviceIdentity: getDeviceUUID(features) || undefined,
@@ -199,12 +206,12 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
199
206
  bindings: [
200
207
  {
201
208
  target: 'bootloader',
202
- artifact: payload.artifact,
209
+ artifact: payload.artifact ?? plannedArtifact,
203
210
  },
204
211
  ],
205
212
  });
206
213
  const source = await openFirmwareByteSource({
207
- artifact: payload.artifact,
214
+ artifact: plannedArtifact,
208
215
  reader: executionParams.artifactReader,
209
216
  });
210
217
  if (!source) {
@@ -215,13 +222,11 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
215
222
  }
216
223
  try {
217
224
  const deviceType = device.getCurrentDeviceType();
218
- const deviceFirmwareType = device.getCurrentFirmwareType();
219
- const firmwareType = payload.firmwareType ?? deviceFirmwareType;
220
225
  if (DeviceModelToTypes.model_touch.includes(deviceType)) {
221
226
  return await this.updateTouchBootloader({
222
227
  device,
223
228
  features,
224
- firmwareType,
229
+ firmwareType: preparedPlan.firmwareType,
225
230
  source,
226
231
  });
227
232
  }
@@ -0,0 +1,207 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
4
+ import JSZip from 'jszip';
5
+
6
+ import { openFirmwareByteSource, readFirmwareByteSourceFully } from './FirmwareArtifactSource';
7
+ import { getFirmwareUpdateResourceName } from './FirmwareUpdatePreparedPlan';
8
+
9
+ import type { FirmwareArtifactReader } from '../../types/api/firmwareUpdate';
10
+ import type { FirmwareUpdatePreparedPlan } from '../../types/api/firmwareUpdatePreparedPlan';
11
+
12
+ const PREPARED_RESOURCE_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
13
+ const PREPARED_RESOURCE_ENTRY_MAX_COUNT = 512;
14
+
15
+ type JSZipSizedEntry = JSZip.JSZipObject & {
16
+ _data?: {
17
+ compressedSize?: unknown;
18
+ uncompressedSize?: unknown;
19
+ };
20
+ };
21
+
22
+ export type VerifiedPreparedResourceEntry = {
23
+ entryName: string;
24
+ binary: ArrayBuffer;
25
+ };
26
+
27
+ const resourceArchiveError = (
28
+ message: string,
29
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' | 'FirmwareArtifactReceiptMismatch'
30
+ ): never => {
31
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, { firmwareUpdateCode });
32
+ };
33
+
34
+ const getZipEntrySizes = (entry: JSZip.JSZipObject) => {
35
+ const { compressedSize, uncompressedSize } = (entry as JSZipSizedEntry)._data ?? {};
36
+ if (
37
+ !Number.isSafeInteger(compressedSize) ||
38
+ Number(compressedSize) < 0 ||
39
+ !Number.isSafeInteger(uncompressedSize) ||
40
+ Number(uncompressedSize) <= 0
41
+ ) {
42
+ return resourceArchiveError(
43
+ `Firmware prepared resource ZIP entry size is invalid: ${entry.name}`,
44
+ 'FirmwareArtifactsNotPrepared'
45
+ );
46
+ }
47
+ return {
48
+ compressedSize: Number(compressedSize),
49
+ uncompressedSize: Number(uncompressedSize),
50
+ };
51
+ };
52
+
53
+ /**
54
+ * 从 PreparedPlan 批准的 ZIP 本体生成资源条目。
55
+ * 宿主提供的 materializedEntries 仅作为 receipt,设备写入始终使用 ZIP 内的规范字节。
56
+ */
57
+ export const readVerifiedPreparedResourceArchive = async ({
58
+ preparedPlan,
59
+ reader,
60
+ }: {
61
+ preparedPlan: FirmwareUpdatePreparedPlan;
62
+ reader: FirmwareArtifactReader | undefined;
63
+ }): Promise<VerifiedPreparedResourceEntry[]> => {
64
+ const resourceArtifacts = preparedPlan.artifacts.filter(
65
+ artifact => artifact.target === 'resource'
66
+ );
67
+ if (!preparedPlan.targetsToUpdate.includes('resource')) {
68
+ if (resourceArtifacts.length > 0) {
69
+ return resourceArchiveError(
70
+ 'Firmware prepared resource artifact is outside the approved targets',
71
+ 'FirmwareArtifactsNotPrepared'
72
+ );
73
+ }
74
+ return [];
75
+ }
76
+ if (resourceArtifacts.length !== 1) {
77
+ return resourceArchiveError(
78
+ 'Firmware prepared plan must contain exactly one materialized resource ZIP',
79
+ 'FirmwareArtifactsNotPrepared'
80
+ );
81
+ }
82
+
83
+ const archiveArtifact = resourceArtifacts[0];
84
+ const preparedEntries = archiveArtifact.materializedEntries;
85
+ if (
86
+ archiveArtifact.role !== 'resource' ||
87
+ archiveArtifact.container !== 'zip' ||
88
+ !preparedEntries?.length
89
+ ) {
90
+ return resourceArchiveError(
91
+ 'Firmware prepared plan must contain exactly one materialized resource ZIP',
92
+ 'FirmwareArtifactsNotPrepared'
93
+ );
94
+ }
95
+ const archiveSource = await openFirmwareByteSource({
96
+ artifact: archiveArtifact.artifact,
97
+ reader,
98
+ });
99
+ if (!archiveSource) {
100
+ return resourceArchiveError(
101
+ 'Firmware prepared resource ZIP is unavailable',
102
+ 'FirmwareArtifactsNotPrepared'
103
+ );
104
+ }
105
+ if (archiveSource.size > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES) {
106
+ await archiveSource.close().catch(() => undefined);
107
+ return resourceArchiveError(
108
+ 'Firmware prepared resource ZIP exceeds the archive size limit',
109
+ 'FirmwareArtifactsNotPrepared'
110
+ );
111
+ }
112
+
113
+ let archiveBinary: ArrayBuffer;
114
+ try {
115
+ archiveBinary = await readFirmwareByteSourceFully(archiveSource);
116
+ } finally {
117
+ await archiveSource.close().catch(() => undefined);
118
+ }
119
+ const archiveDigest = bytesToHex(sha256(new Uint8Array(archiveBinary)));
120
+ if (archiveDigest !== archiveArtifact.artifact.sha256.toLowerCase()) {
121
+ return resourceArchiveError(
122
+ 'Firmware prepared resource ZIP does not match its approved receipt',
123
+ 'FirmwareArtifactReceiptMismatch'
124
+ );
125
+ }
126
+
127
+ let zip: JSZip;
128
+ try {
129
+ zip = await JSZip.loadAsync(archiveBinary);
130
+ } catch {
131
+ return resourceArchiveError(
132
+ 'Firmware prepared resource ZIP cannot be parsed',
133
+ 'FirmwareArtifactsNotPrepared'
134
+ );
135
+ }
136
+ const zipEntries = Object.values(zip.files);
137
+ if (
138
+ zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
139
+ ) {
140
+ return resourceArchiveError(
141
+ 'Firmware prepared resource ZIP contains an unsafe entry path',
142
+ 'FirmwareArtifactsNotPrepared'
143
+ );
144
+ }
145
+ const files = zipEntries.filter(entry => !entry.dir);
146
+ if (files.length === 0 || files.length > PREPARED_RESOURCE_ENTRY_MAX_COUNT) {
147
+ return resourceArchiveError(
148
+ 'Firmware prepared resource ZIP entry set is invalid',
149
+ 'FirmwareArtifactsNotPrepared'
150
+ );
151
+ }
152
+
153
+ let totalSize = 0;
154
+ const canonicalNames = new Set<string>();
155
+ for (const entry of files) {
156
+ const { compressedSize, uncompressedSize } = getZipEntrySizes(entry);
157
+ const resourceName = getFirmwareUpdateResourceName(entry.name);
158
+ const canonicalName = resourceName.toLowerCase();
159
+ totalSize += uncompressedSize;
160
+ if (
161
+ compressedSize > archiveBinary.byteLength ||
162
+ uncompressedSize > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES ||
163
+ totalSize > PREPARED_RESOURCE_ARCHIVE_MAX_BYTES ||
164
+ canonicalNames.has(canonicalName)
165
+ ) {
166
+ return resourceArchiveError(
167
+ `Firmware prepared resource ZIP entry bounds are invalid: ${entry.name}`,
168
+ 'FirmwareArtifactsNotPrepared'
169
+ );
170
+ }
171
+ canonicalNames.add(canonicalName);
172
+ }
173
+
174
+ const preparedEntriesByName = new Map(
175
+ preparedEntries.map(entry => [entry.entryName, entry] as const)
176
+ );
177
+ if (preparedEntriesByName.size !== files.length || preparedEntries.length !== files.length) {
178
+ return resourceArchiveError(
179
+ 'Firmware prepared resource entries do not match the approved ZIP',
180
+ 'FirmwareArtifactReceiptMismatch'
181
+ );
182
+ }
183
+
184
+ const verifiedEntries: VerifiedPreparedResourceEntry[] = [];
185
+ for (const entry of files) {
186
+ const { uncompressedSize } = getZipEntrySizes(entry);
187
+ const binary = await entry.async('arraybuffer');
188
+ const preparedEntry = preparedEntriesByName.get(entry.name);
189
+ const digest = bytesToHex(sha256(new Uint8Array(binary)));
190
+ if (
191
+ binary.byteLength !== uncompressedSize ||
192
+ !preparedEntry ||
193
+ preparedEntry.artifact.size !== binary.byteLength ||
194
+ preparedEntry.artifact.sha256.toLowerCase() !== digest
195
+ ) {
196
+ return resourceArchiveError(
197
+ `Firmware prepared resource entry does not match the approved ZIP: ${entry.name}`,
198
+ 'FirmwareArtifactReceiptMismatch'
199
+ );
200
+ }
201
+ verifiedEntries.push({
202
+ entryName: getFirmwareUpdateResourceName(entry.name),
203
+ binary,
204
+ });
205
+ }
206
+ return verifiedEntries;
207
+ };
@@ -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);
@@ -95,7 +95,7 @@ const asIntegrity = ({
95
95
  }: {
96
96
  size: unknown;
97
97
  sha256: unknown;
98
- }): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
98
+ }): Partial<Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'>> => {
99
99
  const sha256Value =
100
100
  typeof value === 'string' && /^[a-f0-9]{64}$/iu.test(value) ? value.toLowerCase() : undefined;
101
101
  return {
@@ -172,6 +172,20 @@ const planError = (message: string): never => {
172
172
  });
173
173
  };
174
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
+
175
189
  const FIRMWARE_UPDATE_PLAN_FORCE_TARGETS = new Set<FirmwareUpdatePlanForceTarget>([
176
190
  'firmware',
177
191
  'ble',
@@ -309,8 +323,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
309
323
  }
310
324
  assertExactKeys(
311
325
  artifact,
312
- ['artifactId', 'role', 'target', 'url', 'container'],
313
- ['logicalName', 'expectedSize', 'expectedSha256', 'targetVersion']
326
+ ['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
327
+ ['logicalName', 'targetVersion']
314
328
  );
315
329
  const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
316
330
  if (artifactIds.has(artifactId)) {
@@ -329,16 +343,12 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
329
343
  if (artifact.logicalName !== undefined) {
330
344
  assertBoundedString(artifact.logicalName, 'logical name', 256);
331
345
  }
332
- if (
333
- artifact.expectedSize !== undefined &&
334
- (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
335
- ) {
346
+ if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
336
347
  return planError('Firmware update plan artifact size is invalid');
337
348
  }
338
349
  if (
339
- artifact.expectedSha256 !== undefined &&
340
- (typeof artifact.expectedSha256 !== 'string' ||
341
- !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
350
+ typeof artifact.expectedSha256 !== 'string' ||
351
+ !/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
342
352
  ) {
343
353
  return planError('Firmware update plan artifact digest is invalid');
344
354
  }
@@ -502,13 +512,10 @@ const buildProtocolV2Artifacts = (
502
512
  );
503
513
  }
504
514
  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
- }
515
+ const integrity = requireArtifactIntegrity(
516
+ { size: component.expectedSize, sha256: component.fingerprint },
517
+ `Protocol V2 component ${key}`
518
+ );
512
519
  artifacts.push({
513
520
  artifactId: `component:${target}`,
514
521
  role: 'component',
@@ -714,13 +721,10 @@ export const buildProtocolV2FirmwareUpdatePlan = ({
714
721
  if (!archive) {
715
722
  return planError('Protocol V2 resource target has no resource archive');
716
723
  }
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
+ const integrity = requireArtifactIntegrity(
725
+ { size: archive.archiveSize, sha256: archive.archiveSha256 },
726
+ 'Protocol V2 resource archive'
727
+ );
724
728
  protocolV2.artifacts.push({
725
729
  artifactId: 'resource:archive',
726
730
  role: 'resourceBundle',
@@ -791,7 +795,11 @@ export const buildFirmwareUpdatePlan = ({
791
795
  !!asRelease(release);
792
796
  const shouldUpdateFirmware =
793
797
  isUpgrade(firmware) || isRecoveryInstall(firmware) || forcedTargets.has('firmware');
794
- const shouldUpdateResource = isUpgrade(firmware) || forcedTargets.has('resource');
798
+ // V2 cannot write legacy resources once the device is already in bootloader
799
+ // recovery. Do not approve an artifact that its executor cannot apply.
800
+ const shouldUpdateResource =
801
+ !(executor === 'v2' && bootloaderMode) &&
802
+ (isUpgrade(firmware) || forcedTargets.has('resource'));
795
803
 
796
804
  if (
797
805
  executor === 'v4' &&
@@ -808,19 +816,24 @@ export const buildFirmwareUpdatePlan = ({
808
816
  targetsToUpdate = protocolV2.targets;
809
817
  } else {
810
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
+ );
811
827
  artifacts.push({
812
828
  artifactId: 'bootloader',
813
829
  role: 'bootloader',
814
830
  target: 'bootloader',
815
- url: assertArtifactUrl(asRelease(bootloader)?.bootloaderResource, 'Bootloader release'),
831
+ url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
816
832
  container: 'raw',
817
- ...asIntegrity({
818
- size: asRelease(bootloader)?.bootloaderExpectedSize,
819
- sha256: asRelease(bootloader)?.bootloaderFingerprint,
820
- }),
821
- ...(asVersion(asRelease(bootloader)?.bootloaderVersion)
833
+ ...integrity,
834
+ ...(asVersion(bootloaderRelease?.bootloaderVersion)
822
835
  ? {
823
- targetVersion: asVersion(asRelease(bootloader)?.bootloaderVersion),
836
+ targetVersion: asVersion(bootloaderRelease?.bootloaderVersion),
824
837
  }
825
838
  : {}),
826
839
  });
@@ -832,16 +845,17 @@ export const buildFirmwareUpdatePlan = ({
832
845
  });
833
846
  }
834
847
  if (shouldUpdateFirmware) {
848
+ const integrity = requireArtifactIntegrity(
849
+ { size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
850
+ 'Firmware release'
851
+ );
835
852
  artifacts.push({
836
853
  artifactId: 'firmware',
837
854
  role: 'firmware',
838
855
  target: 'firmware',
839
856
  url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
840
857
  container: 'raw',
841
- ...asIntegrity({
842
- size: firmwareRelease.expectedSize,
843
- sha256: firmwareRelease.fingerprint,
844
- }),
858
+ ...integrity,
845
859
  ...(asVersion(firmwareRelease.version)
846
860
  ? { targetVersion: asVersion(firmwareRelease.version) }
847
861
  : {}),
@@ -854,13 +868,8 @@ export const buildFirmwareUpdatePlan = ({
854
868
  platform,
855
869
  });
856
870
  if (resourceUrl) {
857
- artifacts.push({
858
- artifactId: 'resource',
859
- role: 'resource',
860
- target: 'resource',
861
- url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
862
- container: 'zip',
863
- ...asIntegrity({
871
+ const integrity = requireArtifactIntegrity(
872
+ {
864
873
  size:
865
874
  resourceUrl === asString(firmwareRelease.fullResource)
866
875
  ? firmwareRelease.fullResourceExpectedSize
@@ -869,23 +878,36 @@ export const buildFirmwareUpdatePlan = ({
869
878
  resourceUrl === asString(firmwareRelease.fullResource)
870
879
  ? firmwareRelease.fullResourceFingerprint
871
880
  : firmwareRelease.resourceFingerprint,
872
- }),
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,
873
891
  });
874
892
  }
875
893
  }
876
894
  }
877
895
  if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
878
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
+ );
879
904
  artifacts.push({
880
905
  artifactId: 'ble',
881
906
  role: 'ble',
882
907
  target: 'ble',
883
908
  url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
884
909
  container: 'raw',
885
- ...asIntegrity({
886
- size: bleRelease?.expectedSize,
887
- sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
888
- }),
910
+ ...integrity,
889
911
  ...(asVersion(bleRelease?.version)
890
912
  ? { targetVersion: asVersion(bleRelease?.version) }
891
913
  : {}),
@@ -302,6 +302,23 @@ export const validateFirmwareUpdatePreparedPlan = (value: unknown): FirmwareUpda
302
302
  return preparedPlan;
303
303
  };
304
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
+
305
322
  /**
306
323
  * Identity observed on the live device while a degraded recovery plan runs, keyed by
307
324
  * the plan's opaque lease. Bootloader recovery starts with no serial and the device
@@ -8,6 +8,7 @@ export type DeviceUpdateBootloaderParams = {
8
8
  preparedPlan?: FirmwareUpdatePreparedPlan;
9
9
  hostBindingGeneration?: number;
10
10
  binary?: ArrayBuffer;
11
+ /** @deprecated Core derives the bootloader artifact from preparedPlan. */
11
12
  artifact?: FirmwareArtifactReference;
12
13
  artifactReader?: FirmwareArtifactReader;
13
14
  firmwareType?: EFirmwareType;
@@ -38,7 +38,8 @@ export interface FirmwareUpdateBinaryParams {
38
38
  export interface FirmwareUpdateArtifactParams {
39
39
  preparedPlan: FirmwareUpdatePreparedPlan;
40
40
  hostBindingGeneration: number;
41
- artifact: FirmwareArtifactReference;
41
+ /** @deprecated Core derives the component artifact from preparedPlan. */
42
+ artifact?: FirmwareArtifactReference;
42
43
  resourceEntries?: Array<{
43
44
  entryName: string;
44
45
  artifact: FirmwareArtifactReference;
@@ -20,8 +20,8 @@ export interface FirmwareUpdatePlanArtifact {
20
20
  url: string;
21
21
  container: 'raw' | 'zip';
22
22
  logicalName?: string;
23
- expectedSize?: number;
24
- expectedSha256?: string;
23
+ expectedSize: number;
24
+ expectedSha256: string;
25
25
  targetVersion?: string;
26
26
  }
27
27