@onekeyfe/hd-core 1.2.0-alpha.78 → 1.2.0-alpha.79

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__/check-all-firmware-release-protocol-v2.test.ts +12 -71
  2. package/__tests__/protocol-v2-resources.test.ts +102 -222
  3. package/__tests__/protocol-v2.test.ts +110 -380
  4. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  5. package/dist/api/FirmwareUpdateV4.d.ts +0 -6
  6. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  7. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  8. package/dist/data-manager/DataManager.d.ts +1 -2
  9. package/dist/data-manager/DataManager.d.ts.map +1 -1
  10. package/dist/index.d.ts +74 -32
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +255 -499
  13. package/dist/inject.d.ts.map +1 -1
  14. package/dist/protocols/protocol-v2/resources.d.ts +31 -28
  15. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  16. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -0
  17. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/types/api/export.d.ts +1 -0
  19. package/dist/types/api/export.d.ts.map +1 -1
  20. package/dist/types/api/index.d.ts +2 -0
  21. package/dist/types/api/index.d.ts.map +1 -1
  22. package/dist/types/api/protocolV2ResourceManifest.d.ts +17 -0
  23. package/dist/types/api/protocolV2ResourceManifest.d.ts.map +1 -0
  24. package/dist/types/settings.d.ts +30 -21
  25. package/dist/types/settings.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/api/CheckAllFirmwareRelease.ts +5 -34
  28. package/src/api/FirmwareUpdateV4.ts +48 -220
  29. package/src/api/UploadPortfolio.ts +18 -0
  30. package/src/data-manager/DataManager.ts +2 -6
  31. package/src/index.ts +6 -0
  32. package/src/inject.ts +2 -0
  33. package/src/protocols/protocol-v2/resources.ts +193 -324
  34. package/src/types/api/checkAllFirmwareRelease.ts +1 -0
  35. package/src/types/api/export.ts +4 -0
  36. package/src/types/api/index.ts +2 -0
  37. package/src/types/api/protocolV2ResourceManifest.ts +19 -0
  38. package/src/types/settings.ts +30 -36
@@ -1,383 +1,252 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
- import { PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE } from '@onekeyfe/hd-transport';
3
2
 
4
3
  import type {
5
- IProtocolV2BootResources,
6
- IProtocolV2Resource,
7
- IProtocolV2ResourceFile,
8
- IProtocolV2ResourceType,
4
+ IProtocolV2ResourceManifest,
5
+ IProtocolV2ResourceManifestFile,
9
6
  IProtocolV2Resources,
10
7
  } from '../../types';
11
- import type { DeviceCommands } from '../../device/DeviceCommands';
8
+ import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
12
9
 
13
- export const PROTOCOL_V2_RESOURCE_TYPES = [
14
- 'images',
15
- 'animation',
16
- 'wallpaper',
17
- 'translations',
18
- 'roobert',
19
- 'noto',
20
- 'firmware_logo',
21
- ] as const satisfies readonly IProtocolV2ResourceType[];
10
+ export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH =
11
+ 'vol0:/loaders/bootloader/boot_resource.okpkg';
12
+ export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
13
+ export const PROTOCOL_V2_ROM_PARAMS_PACKAGE_PATH = 'vol0:/loaders/rom/params.okpkg';
22
14
 
23
- export const PROTOCOL_V2_RESOURCE_DEVICE_PATHS: Readonly<Record<IProtocolV2ResourceType, string>> =
24
- {
25
- images: 'vol0:/bundles/images/images.okpkg',
26
- animation: 'vol0:/bundles/images/animation.okpkg',
27
- wallpaper: 'vol0:/bundles/images/wallpaper.okpkg',
28
- translations: 'vol0:/bundles/translations/translations.okpkg',
29
- roobert: 'vol0:/bundles/font/roobert.okpkg',
30
- noto: 'vol0:/bundles/font/noto.okpkg',
31
- firmware_logo: 'vol0:/bundles/firmware_logo.okpkg',
32
- };
33
-
34
- const RESOURCE_TYPE_SET = new Set<string>(PROTOCOL_V2_RESOURCE_TYPES);
35
15
  const SHA256_HEX_LENGTH = 64;
36
- const SHA3_512_HEX_LENGTH = 128;
37
- const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
38
- const PROTOCOL_V2_OKPP_TYPE_OFFSET = 0x08;
39
- const PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET = 0x0c;
40
- const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
41
- const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
42
- const PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE =
43
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE;
44
- const PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE = 64;
45
- export const PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS = 5 * 1000;
46
-
47
- export type ProtocolV2ResourceInventoryItem = {
48
- type: IProtocolV2ResourceType;
49
- size: number;
50
- headerHash: string;
51
- };
52
-
53
- export type ProtocolV2ResourceUpdateMode = 'application' | 'bootloader-recovery';
54
16
 
55
- export type ProtocolV2ResourceUpdatePlan = {
56
- status: 'valid' | 'outdated' | 'unknown';
57
- resources: IProtocolV2Resource[];
58
- };
59
-
60
- function toFiniteNumber(value: unknown): number | undefined {
61
- if (typeof value === 'number' && Number.isFinite(value)) return value;
62
- if (typeof value === 'string') {
63
- const numeric = Number(value);
64
- return Number.isFinite(numeric) ? numeric : undefined;
17
+ function normalizeHex(value: unknown, expectedLength: number, field: string): string {
18
+ if (typeof value !== 'string') {
19
+ throw new Error(`Invalid Pro2 resource ${field}: expected a hexadecimal string`);
65
20
  }
66
- if (value && typeof value === 'object') {
67
- const longLike = value as { toNumber?: () => number };
68
- if (typeof longLike.toNumber === 'function') {
69
- const numeric = longLike.toNumber();
70
- return Number.isFinite(numeric) ? numeric : undefined;
71
- }
21
+ const normalized = value.replace(/^0x/i, '').toLowerCase();
22
+ if (normalized.length !== expectedLength || !/^[0-9a-f]+$/.test(normalized)) {
23
+ throw new Error(
24
+ `Invalid Pro2 resource ${field}: expected ${expectedLength} hexadecimal characters`
25
+ );
72
26
  }
73
- return undefined;
27
+ return normalized;
74
28
  }
75
29
 
76
- function toUint8Array(value: unknown): Uint8Array {
77
- if (value instanceof Uint8Array) return value;
78
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
79
- if (ArrayBuffer.isView(value)) {
80
- return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
30
+ /** Validate a complete Pro2 stable resource set from remote configuration. */
31
+ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
32
+ if (value === undefined) return undefined;
33
+ if (!value || typeof value !== 'object') {
34
+ throw new Error('Invalid Pro2 resources config');
81
35
  }
82
- if (typeof value === 'string') {
83
- const hex = value.replace(/^0x/i, '');
84
- if (!hex || hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
85
- return new Uint8Array(0);
86
- }
87
- const bytes = new Uint8Array(hex.length / 2);
88
- for (let index = 0; index < bytes.length; index += 1) {
89
- bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
90
- }
91
- return bytes;
36
+ const { source } = value as { source?: unknown };
37
+ if (!source || typeof source !== 'object') {
38
+ throw new Error('Invalid Pro2 resources config: source is required');
92
39
  }
93
- return new Uint8Array(0);
94
- }
95
-
96
- function readAscii(bytes: Uint8Array, offset: number, length: number): string {
97
- return Array.from(bytes.slice(offset, offset + length), byte => String.fromCharCode(byte)).join(
98
- ''
99
- );
40
+ const { manifestUrl } = source as { manifestUrl?: unknown };
41
+ if (typeof manifestUrl !== 'string' || !manifestUrl.startsWith('https://')) {
42
+ throw new Error('Invalid Pro2 resources config: source.manifestUrl must use HTTPS');
43
+ }
44
+ return {
45
+ source: { manifestUrl },
46
+ };
100
47
  }
101
48
 
102
- function parseProtocolV2ResourceHeaderHash(bytes: Uint8Array): string | undefined {
103
- if (bytes.byteLength < PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE) return undefined;
104
- if (readAscii(bytes, 0, 4) !== 'OKPP') return undefined;
105
- if (readAscii(bytes, PROTOCOL_V2_OKPP_TYPE_OFFSET, 4) !== 'RESC') return undefined;
49
+ const PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS = [
50
+ 'vol0:/bundles/',
51
+ 'vol0:/loaders/rom/',
52
+ ] as const;
106
53
 
107
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
54
+ function isAllowedManifestDevicePath(path: string): boolean {
108
55
  if (
109
- view.getUint32(PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET, true) !== PROTOCOL_V2_OKPP_HEADER_SIZE
56
+ !path.endsWith('.okpkg') ||
57
+ path.includes('\\') ||
58
+ path.includes('//') ||
59
+ path.split('/').some(part => part === '.' || part === '..')
110
60
  ) {
111
- return undefined;
61
+ return false;
112
62
  }
113
- return bytesToHex(
114
- bytes.slice(
115
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
116
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
117
- )
118
- );
119
- }
120
-
121
- async function readProtocolV2ResourceIdentity({
122
- commands,
123
- resource,
124
- chunkSize,
125
- timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
126
- }: {
127
- commands: Pick<DeviceCommands, 'typedCall'>;
128
- resource: IProtocolV2Resource;
129
- chunkSize: number;
130
- timeoutMs?: number;
131
- }): Promise<ProtocolV2ResourceInventoryItem | undefined> {
132
- const path = PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type];
133
- const pathInfo = await commands.typedCall(
134
- 'FilesystemPathInfoQuery',
135
- 'FilesystemPathInfo',
136
- { path },
137
- { timeoutMs }
138
- );
139
- const size = toFiniteNumber(pathInfo.message?.size);
140
- if (
141
- !pathInfo.message?.exist ||
142
- pathInfo.message?.directory ||
143
- !Number.isSafeInteger(size) ||
144
- size !== resource.size ||
145
- size < PROTOCOL_V2_OKPP_HEADER_SIZE
146
- ) {
147
- return undefined;
63
+ if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) {
64
+ return true;
148
65
  }
66
+ return PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS.some(root => path.startsWith(root));
67
+ }
149
68
 
150
- const header = new Uint8Array(PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE);
151
- let offset = 0;
152
- while (offset < header.byteLength) {
153
- const readLength = Math.min(chunkSize, header.byteLength - offset);
154
- const response = await commands.typedCall(
155
- 'FilesystemFileRead',
156
- 'FilesystemFile',
157
- {
158
- file: { path, offset, total_size: 0 },
159
- chunk_len: readLength,
160
- },
161
- { timeoutMs }
162
- );
163
- const data = toUint8Array(response.message?.data);
164
- if (data.byteLength === 0) return undefined;
165
- const copied = Math.min(data.byteLength, header.byteLength - offset);
166
- header.set(data.subarray(0, copied), offset);
167
- offset += copied;
69
+ function assertManifestString(value: unknown, field: string): string {
70
+ if (typeof value !== 'string' || value.length === 0) {
71
+ throw new Error(`Invalid Pro2 resource manifest ${field}`);
168
72
  }
169
-
170
- const headerHash = parseProtocolV2ResourceHeaderHash(header);
171
- return headerHash ? { type: resource.type, size, headerHash } : undefined;
73
+ return value;
172
74
  }
173
75
 
174
- /**
175
- * 使用已发布 Pro2 固件支持的文件系统消息构建资源清单。
176
- * 缺失、无法读取或格式错误的文件不会进入清单,因此会被选中重写。
177
- */
178
- export async function readProtocolV2ResourceInventory({
179
- commands,
180
- resources,
181
- chunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
182
- timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
183
- }: {
184
- commands: Pick<DeviceCommands, 'typedCall'>;
185
- resources: readonly IProtocolV2Resource[];
186
- chunkSize?: number;
187
- timeoutMs?: number;
188
- }): Promise<ProtocolV2ResourceInventoryItem[]> {
189
- const normalizedChunkSize = Number.isFinite(chunkSize)
190
- ? Math.max(Math.floor(chunkSize), PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE)
191
- : PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
192
- const inventory: ProtocolV2ResourceInventoryItem[] = [];
193
- for (const resource of resources) {
194
- try {
195
- const item = await readProtocolV2ResourceIdentity({
196
- commands,
197
- resource,
198
- chunkSize: normalizedChunkSize,
199
- timeoutMs,
200
- });
201
- if (item) inventory.push(item);
202
- } catch {
203
- // 单个资源无法读取时按缺失处理,不阻断其余资源的增量检查。
204
- }
76
+ function assertManifestRelativePath(value: unknown, field: string): string {
77
+ const path = assertManifestString(value, field);
78
+ if (
79
+ path.startsWith('/') ||
80
+ path.includes('\\') ||
81
+ path.includes(':') ||
82
+ path.split('/').some(part => !part || part === '.' || part === '..')
83
+ ) {
84
+ throw new Error(`Invalid Pro2 resource manifest ${field}`);
205
85
  }
206
- return inventory;
86
+ return path;
207
87
  }
208
88
 
209
- function normalizeHex(value: unknown, expectedLength: number, field: string): string {
210
- if (typeof value !== 'string') {
211
- throw new Error(`Invalid Pro2 resource ${field}: expected a hexadecimal string`);
89
+ function parseProtocolV2ResourceManifestFile(
90
+ value: unknown,
91
+ index: number
92
+ ): IProtocolV2ResourceManifestFile {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
94
+ throw new Error(`Invalid Pro2 resource manifest files[${index}]`);
212
95
  }
213
- const normalized = value.replace(/^0x/i, '').toLowerCase();
214
- if (normalized.length !== expectedLength || !/^[0-9a-f]+$/.test(normalized)) {
215
- throw new Error(
216
- `Invalid Pro2 resource ${field}: expected ${expectedLength} hexadecimal characters`
217
- );
96
+ const file = value as Partial<IProtocolV2ResourceManifestFile>;
97
+ const archivePath = assertManifestRelativePath(file.archive_path, `files[${index}].archive_path`);
98
+ const originalName = assertManifestRelativePath(
99
+ file.original_name,
100
+ `files[${index}].original_name`
101
+ );
102
+ if (originalName.includes('/')) {
103
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].original_name`);
218
104
  }
219
- return normalized;
220
- }
221
-
222
- function validateResource(value: unknown, index: number): IProtocolV2Resource {
223
- if (!value || typeof value !== 'object') {
224
- throw new Error(`Invalid Pro2 resource at stable[${index}]`);
105
+ const devicePath = assertManifestString(file.device_path, `files[${index}].device_path`);
106
+ if (!isAllowedManifestDevicePath(devicePath)) {
107
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].device_path`);
108
+ }
109
+ if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
110
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].size`);
225
111
  }
226
- const resource = value as Partial<IProtocolV2Resource>;
227
- if (typeof resource.type !== 'string' || !RESOURCE_TYPE_SET.has(resource.type)) {
228
- throw new Error(`Invalid Pro2 resource type at stable[${index}]`);
112
+ const digest = normalizeHex(file.sha256, SHA256_HEX_LENGTH, `files[${index}].sha256`);
113
+ if (file.signed !== true) {
114
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].signed`);
229
115
  }
230
- if (typeof resource.url !== 'string' || !resource.url.startsWith('https://')) {
231
- throw new Error(`Invalid Pro2 resource url at stable[${index}]`);
116
+ if (file.sig_algo !== 'ed25519' && file.sig_algo !== 'mldsa65') {
117
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].sig_algo`);
232
118
  }
233
- if (!Number.isSafeInteger(resource.size) || Number(resource.size) <= 0) {
234
- throw new Error(`Invalid Pro2 resource size at stable[${index}]`);
119
+ if (file.payload_version !== null && typeof file.payload_version !== 'string') {
120
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].payload_version`);
121
+ }
122
+ if (!archivePath.endsWith('.okpkg') || !originalName.endsWith('.okpkg')) {
123
+ throw new Error(`Invalid Pro2 resource manifest files[${index}] package extension`);
235
124
  }
236
125
  return {
237
- type: resource.type,
238
- url: resource.url,
239
- size: Number(resource.size),
240
- fileHash: normalizeHex(resource.fileHash, SHA256_HEX_LENGTH, 'fileHash'),
241
- headerHash: normalizeHex(resource.headerHash, SHA3_512_HEX_LENGTH, 'headerHash'),
126
+ archive_path: archivePath,
127
+ original_name: originalName,
128
+ device_path: devicePath,
129
+ size: Number(file.size),
130
+ sha256: digest,
131
+ signed: true,
132
+ sig_algo: file.sig_algo,
133
+ payload_version: file.payload_version ?? null,
242
134
  };
243
135
  }
244
136
 
245
- function validateBootResources(value: unknown): IProtocolV2BootResources {
246
- if (!value || typeof value !== 'object') {
247
- throw new Error('Invalid Pro2 boot resources config');
248
- }
249
- const resource = value as Partial<IProtocolV2BootResources>;
250
- if (resource.required !== false) {
251
- throw new Error('Invalid Pro2 boot resources required flag: expected false');
252
- }
253
- if (resource.target !== 'RES') {
254
- throw new Error('Invalid Pro2 boot resources target: expected RES');
137
+ export function parseProtocolV2ResourceManifest(value: unknown): IProtocolV2ResourceManifest {
138
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
139
+ throw new Error('Invalid Pro2 resource manifest');
255
140
  }
141
+ const manifest = value as Partial<IProtocolV2ResourceManifest>;
256
142
  if (
257
- resource.manifestUrl !== undefined &&
258
- (typeof resource.manifestUrl !== 'string' || !resource.manifestUrl.startsWith('https://'))
143
+ manifest.schema !== 1 ||
144
+ manifest.variant !== 'resource' ||
145
+ manifest.device_root !== 'vol0:' ||
146
+ manifest.restore_mode !== 'bootloader_update' ||
147
+ !Array.isArray(manifest.trees) ||
148
+ !Array.isArray(manifest.files)
259
149
  ) {
260
- throw new Error('Invalid Pro2 boot resources manifestUrl');
261
- }
262
- if (!Array.isArray(resource.files) || resource.files.length === 0) {
263
- throw new Error('Invalid Pro2 boot resources files');
150
+ throw new Error('Invalid Pro2 resource manifest contract');
264
151
  }
265
- const files = resource.files.map((value, index): IProtocolV2ResourceFile => {
266
- if (!value || typeof value !== 'object') {
267
- throw new Error(`Invalid Pro2 boot resource file at files[${index}]`);
268
- }
269
- const file = value as Partial<IProtocolV2ResourceFile>;
270
- if (typeof file.url !== 'string' || !file.url.startsWith('https://')) {
271
- throw new Error(`Invalid Pro2 boot resource url at files[${index}]`);
272
- }
273
- if (
274
- typeof file.devicePath !== 'string' ||
275
- !file.devicePath.startsWith('vol0:/') ||
276
- file.devicePath.includes('..') ||
277
- file.devicePath.includes('\\')
278
- ) {
279
- throw new Error(`Invalid Pro2 boot resource devicePath at files[${index}]`);
280
- }
281
- if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
282
- throw new Error(`Invalid Pro2 boot resource size at files[${index}]`);
283
- }
284
- return {
285
- ...(typeof file.name === 'string' && file.name ? { name: file.name } : undefined),
286
- url: file.url,
287
- devicePath: file.devicePath,
288
- size: Number(file.size),
289
- fileHash: normalizeHex(file.fileHash, SHA256_HEX_LENGTH, `boot files[${index}].fileHash`),
290
- };
291
- });
292
- if (new Set(files.map(file => file.devicePath)).size !== files.length) {
293
- throw new Error('Invalid Pro2 boot resources files: duplicate devicePath');
152
+ const files = manifest.files.map(parseProtocolV2ResourceManifestFile);
153
+ const devicePaths = new Set(files.map(file => file.device_path));
154
+ const archivePaths = new Set(files.map(file => file.archive_path));
155
+ if (
156
+ files.length === 0 ||
157
+ devicePaths.size !== files.length ||
158
+ archivePaths.size !== files.length ||
159
+ !devicePaths.has(PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) ||
160
+ !files.some(file => file.device_path.startsWith('vol0:/bundles/'))
161
+ ) {
162
+ throw new Error('Invalid Pro2 resource manifest file set');
294
163
  }
295
164
  return {
296
- required: false,
297
- target: 'RES',
298
- ...(resource.manifestUrl ? { manifestUrl: resource.manifestUrl } : undefined),
165
+ schema: 1,
166
+ artifact_name: assertManifestString(manifest.artifact_name, 'artifact_name'),
167
+ release_name: assertManifestString(manifest.release_name, 'release_name'),
168
+ variant: 'resource',
169
+ commit: assertManifestString(manifest.commit, 'commit'),
170
+ short_sha: assertManifestString(manifest.short_sha, 'short_sha'),
171
+ timestamp_utc: assertManifestString(manifest.timestamp_utc, 'timestamp_utc'),
172
+ core_version: assertManifestString(manifest.core_version, 'core_version'),
173
+ key_set: assertManifestString(manifest.key_set, 'key_set'),
174
+ device_root: 'vol0:',
175
+ restore_mode: 'bootloader_update',
176
+ trees: manifest.trees.map((tree, index) => {
177
+ if (!tree || typeof tree !== 'object') {
178
+ throw new Error(`Invalid Pro2 resource manifest trees[${index}]`);
179
+ }
180
+ const item = tree as { path?: unknown; device?: unknown };
181
+ return {
182
+ path: assertManifestRelativePath(item.path, `trees[${index}].path`),
183
+ device: assertManifestString(item.device, `trees[${index}].device`),
184
+ };
185
+ }),
299
186
  files,
300
187
  };
301
188
  }
302
189
 
303
- /** Validate a complete Pro2 stable resource set from remote configuration. */
304
- export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
305
- if (value === undefined) return undefined;
306
- if (
307
- !value ||
308
- typeof value !== 'object' ||
309
- !Array.isArray((value as { stable?: unknown }).stable)
310
- ) {
311
- throw new Error('Invalid Pro2 resources config: stable must be an array');
312
- }
313
-
314
- const config = value as { stable: unknown[]; boot?: unknown };
315
- const stable = config.stable.map(validateResource);
316
- const types = new Set(stable.map(resource => resource.type));
317
- if (stable.length !== PROTOCOL_V2_RESOURCE_TYPES.length || types.size !== stable.length) {
318
- throw new Error(
319
- `Invalid Pro2 resources config: stable must contain ${PROTOCOL_V2_RESOURCE_TYPES.length} unique resource types`
320
- );
321
- }
322
- for (const type of PROTOCOL_V2_RESOURCE_TYPES) {
323
- if (!types.has(type)) {
324
- throw new Error(`Invalid Pro2 resources config: stable is missing ${type}`);
190
+ export function selectProtocolV2ResourceManifestFiles({
191
+ manifest,
192
+ targetsToUpdate,
193
+ }: {
194
+ manifest: IProtocolV2ResourceManifest;
195
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
196
+ }): IProtocolV2ResourceManifestFile[] {
197
+ const targets = new Set(targetsToUpdate);
198
+ return manifest.files.filter(file => {
199
+ if (file.device_path.startsWith('vol0:/bundles/')) {
200
+ return targets.has('resource');
325
201
  }
202
+ return targets.has('boot_resources');
203
+ });
204
+ }
205
+
206
+ export function resolveProtocolV2ResourceManifestFileUrl({
207
+ manifestUrl,
208
+ archivePath,
209
+ }: {
210
+ manifestUrl: string;
211
+ archivePath: string;
212
+ }): string {
213
+ const url = new URL(assertManifestRelativePath(archivePath, 'archive_path'), manifestUrl);
214
+ if (url.protocol !== 'https:') {
215
+ throw new Error('Invalid Pro2 resource manifest file URL');
326
216
  }
327
- const boot = config.boot === undefined ? undefined : validateBootResources(config.boot);
328
- return {
329
- stable: PROTOCOL_V2_RESOURCE_TYPES.map(type => {
330
- const resource = stable.find(item => item.type === type);
331
- if (!resource) {
332
- throw new Error(`Invalid Pro2 resources config: stable is missing ${type}`);
333
- }
334
- return resource;
335
- }),
336
- ...(boot ? { boot } : undefined),
337
- };
217
+ return url.toString();
338
218
  }
339
219
 
340
- /** 比较文件系统资源清单;恢复模式无法取得清单时回退到全量更新。 */
341
- export function buildProtocolV2ResourceUpdatePlan({
342
- resources,
343
- inventory,
344
- mode,
345
- forced = false,
220
+ export function prepareProtocolV2ResourceFiles({
221
+ manifest: value,
222
+ files,
223
+ targetsToUpdate,
346
224
  }: {
347
- resources: readonly IProtocolV2Resource[];
348
- inventory?: readonly ProtocolV2ResourceInventoryItem[];
349
- mode: ProtocolV2ResourceUpdateMode;
350
- forced?: boolean;
351
- }): ProtocolV2ResourceUpdatePlan {
352
- if (forced) {
225
+ manifest: unknown;
226
+ files: Array<{ archivePath: string; binary: ArrayBuffer }>;
227
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
228
+ }): Array<{ binary: ArrayBuffer; devicePath: string; size: number; fileHash: string }> {
229
+ const manifest = parseProtocolV2ResourceManifest(value);
230
+ const selected = selectProtocolV2ResourceManifestFiles({ manifest, targetsToUpdate });
231
+ const binaries = new Map(files.map(file => [file.archivePath, file.binary] as const));
232
+ return selected.map(file => {
233
+ const binary = binaries.get(file.archive_path);
234
+ if (
235
+ !binary ||
236
+ !isProtocolV2ResourceFileValid(binary, {
237
+ size: file.size,
238
+ fileHash: file.sha256,
239
+ })
240
+ ) {
241
+ throw new Error(`Pro2 resource manifest file verification failed: ${file.archive_path}`);
242
+ }
353
243
  return {
354
- status: resources.length > 0 ? 'outdated' : 'valid',
355
- resources: [...resources],
244
+ binary,
245
+ devicePath: file.device_path,
246
+ size: file.size,
247
+ fileHash: file.sha256,
356
248
  };
357
- }
358
- if (!inventory) {
359
- return mode === 'bootloader-recovery'
360
- ? {
361
- status: resources.length > 0 ? 'outdated' : 'valid',
362
- resources: [...resources],
363
- }
364
- : { status: 'unknown', resources: [] };
365
- }
366
-
367
- const inventoryByType = new Map(inventory.map(item => [item.type, item]));
368
- const changedResources = resources.filter(resource => {
369
- const current = inventoryByType.get(resource.type);
370
- return (
371
- !current ||
372
- current.size !== resource.size ||
373
- current.headerHash.toLowerCase() !== resource.headerHash.toLowerCase()
374
- );
375
249
  });
376
-
377
- return {
378
- status: changedResources.length === 0 ? 'valid' : 'outdated',
379
- resources: changedResources,
380
- };
381
250
  }
382
251
 
383
252
  function bytesToHex(bytes: Uint8Array): string {
@@ -387,7 +256,7 @@ function bytesToHex(bytes: Uint8Array): string {
387
256
  /** Verify the complete downloaded file before any device mutation. */
388
257
  export function isProtocolV2ResourceFileValid(
389
258
  binary: ArrayBuffer,
390
- resource: Pick<IProtocolV2ResourceFile, 'size' | 'fileHash'>
259
+ resource: { size: number; fileHash: string }
391
260
  ): boolean {
392
261
  if (binary.byteLength !== resource.size) return false;
393
262
  return bytesToHex(sha256(new Uint8Array(binary))) === resource.fileHash.toLowerCase();
@@ -61,6 +61,7 @@ export type AllFirmwareRelease = {
61
61
  hasUpgrade?: boolean;
62
62
  required?: boolean;
63
63
  resourceStatus?: 'valid' | 'outdated' | 'unknown';
64
+ resourceManifestUrl?: string;
64
65
  currentVersions?: DeviceStateVersions;
65
66
  components?: ProtocolV2FirmwareComponentRelease[];
66
67
  targetsToUpdate?: FirmwareUpdateV4Target[];
@@ -49,6 +49,10 @@ export type {
49
49
  FirmwareUpdatePreparedEntry,
50
50
  FirmwareUpdatePreparedPlan,
51
51
  } from './firmwareUpdatePreparedPlan';
52
+ export type {
53
+ ProtocolV2PreparedResourceFile,
54
+ ProtocolV2ResourceManifestBinary,
55
+ } from './protocolV2ResourceManifest';
52
56
  export type { FirmwareUpdateCapabilities } from './firmwareUpdateCapabilities';
53
57
  export type { AllFirmwareRelease } from './checkAllFirmwareRelease';
54
58
  export type {
@@ -41,6 +41,7 @@ import type {
41
41
  prepareFirmwareUpdatePlan,
42
42
  validateFirmwareUpdatePreparedPlan,
43
43
  } from './firmwareUpdatePreparedPlan';
44
+ import type { prepareProtocolV2ResourceFiles } from './protocolV2ResourceManifest';
44
45
  import type { promptWebDeviceAccess } from './promptWebDeviceAccess';
45
46
  import type { deviceReset } from './deviceReset';
46
47
  import type { deviceRecovery } from './deviceRecovery';
@@ -253,6 +254,7 @@ export type CoreApi = {
253
254
  getFirmwareUpdateCapabilities: typeof getFirmwareUpdateCapabilities;
254
255
  prepareFirmwareUpdatePlan: typeof prepareFirmwareUpdatePlan;
255
256
  validateFirmwareUpdatePreparedPlan: typeof validateFirmwareUpdatePreparedPlan;
257
+ prepareProtocolV2ResourceFiles: typeof prepareProtocolV2ResourceFiles;
256
258
  cipherKeyValue: typeof cipherKeyValue;
257
259
 
258
260
  /**
@@ -0,0 +1,19 @@
1
+ import type { FirmwareUpdateV4Target } from './firmwareUpdate';
2
+
3
+ export type ProtocolV2ResourceManifestBinary = {
4
+ archivePath: string;
5
+ binary: ArrayBuffer;
6
+ };
7
+
8
+ export type ProtocolV2PreparedResourceFile = {
9
+ binary: ArrayBuffer;
10
+ devicePath: string;
11
+ size: number;
12
+ fileHash: string;
13
+ };
14
+
15
+ export declare function prepareProtocolV2ResourceFiles(input: {
16
+ manifest: unknown;
17
+ files: ProtocolV2ResourceManifestBinary[];
18
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
19
+ }): ProtocolV2PreparedResourceFile[];