@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.
Files changed (98) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +112 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
  9. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  10. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  12. package/__tests__/protocol-v2-resources.test.ts +148 -232
  13. package/__tests__/protocol-v2.test.ts +1236 -516
  14. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  15. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  16. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  17. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  19. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  20. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  21. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +8 -8
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  28. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  31. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  32. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  34. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  35. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  36. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  37. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  38. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  39. package/dist/api/firmware/getBinary.d.ts +0 -1
  40. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  41. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  42. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  43. package/dist/data-manager/DataManager.d.ts +4 -3
  44. package/dist/data-manager/DataManager.d.ts.map +1 -1
  45. package/dist/index.d.ts +108 -114
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1633 -967
  48. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  51. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  52. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  53. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  54. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  55. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  59. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  60. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/types/api/export.d.ts +1 -1
  62. package/dist/types/api/export.d.ts.map +1 -1
  63. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  64. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  66. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  67. package/dist/types/settings.d.ts +35 -36
  68. package/dist/types/settings.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +108 -80
  71. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  72. package/src/api/CheckBootloaderRelease.ts +35 -3
  73. package/src/api/CheckFirmwareRelease.ts +35 -7
  74. package/src/api/FirmwareUpdateV2.ts +108 -73
  75. package/src/api/FirmwareUpdateV3.ts +101 -56
  76. package/src/api/FirmwareUpdateV4.ts +885 -403
  77. package/src/api/UploadPortfolio.ts +18 -0
  78. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  79. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  80. package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
  81. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  82. package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
  83. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
  84. package/src/api/firmware/protocolV2Release.ts +168 -0
  85. package/src/data-manager/DataManager.ts +35 -19
  86. package/src/index.ts +10 -0
  87. package/src/protocols/protocol-v2/resources.ts +157 -341
  88. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  89. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  90. package/src/types/api/checkBootloaderRelease.ts +2 -6
  91. package/src/types/api/checkFirmwareRelease.ts +2 -13
  92. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  93. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  94. package/src/types/api/export.ts +6 -1
  95. package/src/types/api/firmwareUpdate.ts +12 -17
  96. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  97. package/src/types/settings.ts +35 -61
  98. package/src/utils/deviceFeaturesUtils.ts +2 -2
@@ -1,210 +1,16 @@
1
- import { sha256 } from '@noble/hashes/sha256';
2
- import { PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE } from '@onekeyfe/hd-transport';
3
-
4
1
  import type {
5
- IProtocolV2BootResources,
6
- IProtocolV2Resource,
7
- IProtocolV2ResourceFile,
8
- IProtocolV2ResourceType,
2
+ IProtocolV2ResourceManifest,
3
+ IProtocolV2ResourceManifestFile,
9
4
  IProtocolV2Resources,
10
5
  } from '../../types';
11
- import type { DeviceCommands } from '../../device/DeviceCommands';
12
-
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[];
6
+ import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
22
7
 
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
- };
8
+ export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH =
9
+ 'vol0:/loaders/bootloader/boot_resource.okpkg';
10
+ export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
11
+ export const PROTOCOL_V2_ROM_PARAMS_PACKAGE_PATH = 'vol0:/loaders/rom/params.okpkg';
33
12
 
34
- const RESOURCE_TYPE_SET = new Set<string>(PROTOCOL_V2_RESOURCE_TYPES);
35
13
  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
-
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;
65
- }
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
- }
72
- }
73
- return undefined;
74
- }
75
-
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);
81
- }
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;
92
- }
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
- );
100
- }
101
-
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;
106
-
107
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
108
- if (
109
- view.getUint32(PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET, true) !== PROTOCOL_V2_OKPP_HEADER_SIZE
110
- ) {
111
- return undefined;
112
- }
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;
148
- }
149
-
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;
168
- }
169
-
170
- const headerHash = parseProtocolV2ResourceHeaderHash(header);
171
- return headerHash ? { type: resource.type, size, headerHash } : undefined;
172
- }
173
-
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
- }
205
- }
206
- return inventory;
207
- }
208
14
 
209
15
  function normalizeHex(value: unknown, expectedLength: number, field: string): string {
210
16
  if (typeof value !== 'string') {
@@ -219,176 +25,186 @@ function normalizeHex(value: unknown, expectedLength: number, field: string): st
219
25
  return normalized;
220
26
  }
221
27
 
222
- function validateResource(value: unknown, index: number): IProtocolV2Resource {
28
+ /** Validate a complete Pro2 stable resource set from remote configuration. */
29
+ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
30
+ if (value === undefined) return undefined;
223
31
  if (!value || typeof value !== 'object') {
224
- throw new Error(`Invalid Pro2 resource at stable[${index}]`);
32
+ throw new Error('Invalid Pro2 resources config');
225
33
  }
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}]`);
34
+ const { source } = value as { source?: unknown };
35
+ if (!source || typeof source !== 'object') {
36
+ throw new Error('Invalid Pro2 resources config: source is required');
229
37
  }
230
- if (typeof resource.url !== 'string' || !resource.url.startsWith('https://')) {
231
- throw new Error(`Invalid Pro2 resource url at stable[${index}]`);
38
+ const { archiveUrl, archiveSha256, archiveSize } = source as {
39
+ archiveUrl?: unknown;
40
+ archiveSha256?: unknown;
41
+ archiveSize?: unknown;
42
+ };
43
+ if (typeof archiveUrl !== 'string' || !archiveUrl.startsWith('https://')) {
44
+ throw new Error('Invalid Pro2 resources config: source.archiveUrl must use HTTPS');
45
+ }
46
+ if (typeof archiveSha256 !== 'string' || !/^[0-9a-fA-F]{64}$/.test(archiveSha256)) {
47
+ throw new Error('Invalid Pro2 resources config: source.archiveSha256 must be a SHA-256 digest');
232
48
  }
233
- if (!Number.isSafeInteger(resource.size) || Number(resource.size) <= 0) {
234
- throw new Error(`Invalid Pro2 resource size at stable[${index}]`);
49
+ if (!Number.isSafeInteger(archiveSize) || (archiveSize as number) <= 0) {
50
+ throw new Error('Invalid Pro2 resources config: source.archiveSize must be a positive integer');
235
51
  }
236
52
  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'),
53
+ source: {
54
+ archiveUrl,
55
+ archiveSha256: archiveSha256.toLowerCase(),
56
+ archiveSize: archiveSize as number,
57
+ },
242
58
  };
243
59
  }
244
60
 
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');
255
- }
61
+ const PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS = [
62
+ 'vol0:/bundles/',
63
+ 'vol0:/loaders/rom/',
64
+ ] as const;
65
+
66
+ function isAllowedManifestDevicePath(path: string): boolean {
256
67
  if (
257
- resource.manifestUrl !== undefined &&
258
- (typeof resource.manifestUrl !== 'string' || !resource.manifestUrl.startsWith('https://'))
68
+ !path.endsWith('.okpkg') ||
69
+ path.includes('\\') ||
70
+ path.includes('//') ||
71
+ path.split('/').some(part => part === '.' || part === '..')
259
72
  ) {
260
- throw new Error('Invalid Pro2 boot resources manifestUrl');
73
+ return false;
261
74
  }
262
- if (!Array.isArray(resource.files) || resource.files.length === 0) {
263
- throw new Error('Invalid Pro2 boot resources files');
75
+ if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) {
76
+ return true;
264
77
  }
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');
78
+ return PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS.some(root => path.startsWith(root));
79
+ }
80
+
81
+ function assertManifestString(value: unknown, field: string): string {
82
+ if (typeof value !== 'string' || value.length === 0) {
83
+ throw new Error(`Invalid Pro2 resource manifest ${field}`);
294
84
  }
295
- return {
296
- required: false,
297
- target: 'RES',
298
- ...(resource.manifestUrl ? { manifestUrl: resource.manifestUrl } : undefined),
299
- files,
300
- };
85
+ return value;
301
86
  }
302
87
 
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;
88
+ function assertManifestRelativePath(value: unknown, field: string): string {
89
+ const path = assertManifestString(value, field);
306
90
  if (
307
- !value ||
308
- typeof value !== 'object' ||
309
- !Array.isArray((value as { stable?: unknown }).stable)
91
+ path.startsWith('/') ||
92
+ path.includes('\\') ||
93
+ path.includes(':') ||
94
+ path.split('/').some(part => !part || part === '.' || part === '..')
310
95
  ) {
311
- throw new Error('Invalid Pro2 resources config: stable must be an array');
96
+ throw new Error(`Invalid Pro2 resource manifest ${field}`);
312
97
  }
98
+ return path;
99
+ }
313
100
 
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
- );
101
+ function parseProtocolV2ResourceManifestFile(
102
+ value: unknown,
103
+ index: number
104
+ ): IProtocolV2ResourceManifestFile {
105
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
106
+ throw new Error(`Invalid Pro2 resource manifest files[${index}]`);
107
+ }
108
+ const file = value as Partial<IProtocolV2ResourceManifestFile>;
109
+ const archivePath = assertManifestRelativePath(file.archive_path, `files[${index}].archive_path`);
110
+ const originalName = assertManifestRelativePath(
111
+ file.original_name,
112
+ `files[${index}].original_name`
113
+ );
114
+ if (originalName.includes('/')) {
115
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].original_name`);
116
+ }
117
+ const devicePath = assertManifestString(file.device_path, `files[${index}].device_path`);
118
+ if (!isAllowedManifestDevicePath(devicePath)) {
119
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].device_path`);
120
+ }
121
+ if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
122
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].size`);
321
123
  }
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}`);
325
- }
124
+ const digest = normalizeHex(file.sha256, SHA256_HEX_LENGTH, `files[${index}].sha256`);
125
+ if (file.signed !== true) {
126
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].signed`);
127
+ }
128
+ if (file.sig_algo !== 'ed25519' && file.sig_algo !== 'mldsa65') {
129
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].sig_algo`);
130
+ }
131
+ if (file.payload_version !== null && typeof file.payload_version !== 'string') {
132
+ throw new Error(`Invalid Pro2 resource manifest files[${index}].payload_version`);
133
+ }
134
+ if (!archivePath.endsWith('.okpkg') || !originalName.endsWith('.okpkg')) {
135
+ throw new Error(`Invalid Pro2 resource manifest files[${index}] package extension`);
326
136
  }
327
- const boot = config.boot === undefined ? undefined : validateBootResources(config.boot);
328
137
  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),
138
+ archive_path: archivePath,
139
+ original_name: originalName,
140
+ device_path: devicePath,
141
+ size: Number(file.size),
142
+ sha256: digest,
143
+ signed: true,
144
+ sig_algo: file.sig_algo,
145
+ payload_version: file.payload_version ?? null,
337
146
  };
338
147
  }
339
148
 
340
- /** 比较文件系统资源清单;恢复模式无法取得清单时回退到全量更新。 */
341
- export function buildProtocolV2ResourceUpdatePlan({
342
- resources,
343
- inventory,
344
- mode,
345
- forced = false,
346
- }: {
347
- resources: readonly IProtocolV2Resource[];
348
- inventory?: readonly ProtocolV2ResourceInventoryItem[];
349
- mode: ProtocolV2ResourceUpdateMode;
350
- forced?: boolean;
351
- }): ProtocolV2ResourceUpdatePlan {
352
- if (forced) {
353
- return {
354
- status: resources.length > 0 ? 'outdated' : 'valid',
355
- resources: [...resources],
356
- };
149
+ export function parseProtocolV2ResourceManifest(value: unknown): IProtocolV2ResourceManifest {
150
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
151
+ throw new Error('Invalid Pro2 resource manifest');
357
152
  }
358
- if (!inventory) {
359
- return mode === 'bootloader-recovery'
360
- ? {
361
- status: resources.length > 0 ? 'outdated' : 'valid',
362
- resources: [...resources],
363
- }
364
- : { status: 'unknown', resources: [] };
153
+ const manifest = value as Partial<IProtocolV2ResourceManifest>;
154
+ if (
155
+ manifest.schema !== 1 ||
156
+ manifest.variant !== 'resource' ||
157
+ manifest.device_root !== 'vol0:' ||
158
+ manifest.restore_mode !== 'bootloader_update' ||
159
+ !Array.isArray(manifest.trees) ||
160
+ !Array.isArray(manifest.files)
161
+ ) {
162
+ throw new Error('Invalid Pro2 resource manifest contract');
163
+ }
164
+ const files = manifest.files.map(parseProtocolV2ResourceManifestFile);
165
+ const devicePaths = new Set(files.map(file => file.device_path));
166
+ const archivePaths = new Set(files.map(file => file.archive_path));
167
+ if (
168
+ files.length === 0 ||
169
+ devicePaths.size !== files.length ||
170
+ archivePaths.size !== files.length ||
171
+ !devicePaths.has(PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) ||
172
+ !files.some(file => file.device_path.startsWith('vol0:/bundles/'))
173
+ ) {
174
+ throw new Error('Invalid Pro2 resource manifest file set');
365
175
  }
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
- });
376
-
377
176
  return {
378
- status: changedResources.length === 0 ? 'valid' : 'outdated',
379
- resources: changedResources,
177
+ schema: 1,
178
+ artifact_name: assertManifestString(manifest.artifact_name, 'artifact_name'),
179
+ release_name: assertManifestString(manifest.release_name, 'release_name'),
180
+ variant: 'resource',
181
+ commit: assertManifestString(manifest.commit, 'commit'),
182
+ short_sha: assertManifestString(manifest.short_sha, 'short_sha'),
183
+ timestamp_utc: assertManifestString(manifest.timestamp_utc, 'timestamp_utc'),
184
+ core_version: assertManifestString(manifest.core_version, 'core_version'),
185
+ key_set: assertManifestString(manifest.key_set, 'key_set'),
186
+ device_root: 'vol0:',
187
+ restore_mode: 'bootloader_update',
188
+ trees: manifest.trees.map((tree, index) => {
189
+ if (!tree || typeof tree !== 'object') {
190
+ throw new Error(`Invalid Pro2 resource manifest trees[${index}]`);
191
+ }
192
+ const item = tree as { path?: unknown; device?: unknown };
193
+ return {
194
+ path: assertManifestRelativePath(item.path, `trees[${index}].path`),
195
+ device: assertManifestString(item.device, `trees[${index}].device`),
196
+ };
197
+ }),
198
+ files,
380
199
  };
381
200
  }
382
201
 
383
- function bytesToHex(bytes: Uint8Array): string {
384
- return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
385
- }
386
-
387
- /** Verify the complete downloaded file before any device mutation. */
388
- export function isProtocolV2ResourceFileValid(
389
- binary: ArrayBuffer,
390
- resource: Pick<IProtocolV2ResourceFile, 'size' | 'fileHash'>
391
- ): boolean {
392
- if (binary.byteLength !== resource.size) return false;
393
- return bytesToHex(sha256(new Uint8Array(binary))) === resource.fileHash.toLowerCase();
202
+ export function selectProtocolV2ResourceManifestFiles({
203
+ manifest,
204
+ targetsToUpdate,
205
+ }: {
206
+ manifest: IProtocolV2ResourceManifest;
207
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
208
+ }): IProtocolV2ResourceManifestFile[] {
209
+ return targetsToUpdate.includes('resource') ? [...manifest.files] : [];
394
210
  }
@@ -1,30 +1,43 @@
1
1
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
2
2
  import type { CommonParams, Response } from '../params';
3
- import type {
4
- DeviceStateVersions,
5
- Features,
6
- IDeviceBLEFirmwareStatus,
7
- IDeviceFirmwareStatus,
8
- } from '../device';
3
+ import type { DeviceStateVersions, Features, IDeviceFirmwareStatus } from '../device';
9
4
  import type {
10
5
  IBLEFirmwareReleaseInfo,
11
6
  IFirmwareReleaseInfo,
7
+ IProtocolV2FirmwareComponent,
12
8
  IProtocolV2FirmwareComponentTarget,
9
+ IProtocolV2ResourceSource,
13
10
  } from '../settings';
14
11
  import type { FirmwareUpdateV4Target } from './firmwareUpdate';
15
12
  import type { FirmwareUpdatePlan, FirmwareUpdatePlanForceTarget } from './firmwareUpdatePlan';
16
13
 
17
- export type FirmwareRelease = {
14
+ export type ProtocolV2ComponentReleaseInfo = IProtocolV2FirmwareComponent & {
15
+ protocol: 'V2';
16
+ configKey: string;
17
+ componentTarget: IProtocolV2FirmwareComponentTarget;
18
+ required: boolean;
19
+ changelog: IFirmwareReleaseInfo['changelog'];
20
+ };
21
+
22
+ export type FirmwareReleaseCheckResult = {
18
23
  shouldUpdate?: boolean;
19
24
  status: IDeviceFirmwareStatus;
20
25
  changelog?: {
21
26
  'zh-CN': string;
22
27
  'en-US': string;
23
28
  }[];
24
- release: IDeviceBLEFirmwareStatus | IBLEFirmwareReleaseInfo | IFirmwareReleaseInfo;
29
+ release:
30
+ | IBLEFirmwareReleaseInfo
31
+ | IFirmwareReleaseInfo
32
+ | ProtocolV2ComponentReleaseInfo
33
+ | undefined;
25
34
  bootloaderMode?: boolean;
26
35
  };
27
36
 
37
+ export type BridgeReleaseCheckResult = Omit<FirmwareReleaseCheckResult, 'release'> & {
38
+ release: string | undefined;
39
+ };
40
+
28
41
  export type ProtocolV2FirmwareComponentReleaseStatus =
29
42
  | 'valid'
30
43
  | 'outdated'
@@ -49,10 +62,10 @@ export type ProtocolV2FirmwareComponentRelease = {
49
62
  };
50
63
 
51
64
  export type AllFirmwareRelease = {
52
- firmware: FirmwareRelease;
53
- ble: FirmwareRelease;
54
- bootloader?: FirmwareRelease;
55
- bridge?: FirmwareRelease;
65
+ firmware: FirmwareReleaseCheckResult;
66
+ ble: FirmwareReleaseCheckResult;
67
+ bootloader?: FirmwareReleaseCheckResult;
68
+ bridge?: BridgeReleaseCheckResult;
56
69
  features?: Features;
57
70
  protocol?: 'V1' | 'V2';
58
71
  deviceType?: 'pro2' | 'neo';
@@ -61,6 +74,7 @@ export type AllFirmwareRelease = {
61
74
  hasUpgrade?: boolean;
62
75
  required?: boolean;
63
76
  resourceStatus?: 'valid' | 'outdated' | 'unknown';
77
+ resourceArchive?: IProtocolV2ResourceSource;
64
78
  currentVersions?: DeviceStateVersions;
65
79
  components?: ProtocolV2FirmwareComponentRelease[];
66
80
  targetsToUpdate?: FirmwareUpdateV4Target[];
@@ -74,6 +88,7 @@ export type CheckAllFirmwareReleaseParams = {
74
88
  firmwareType?: EFirmwareType;
75
89
  platform?: 'native' | 'desktop' | 'ext' | 'web' | 'web-embed';
76
90
  forceUpdateTargets?: FirmwareUpdatePlanForceTarget[];
91
+ protocolV2ForceUpdateTargets?: FirmwareUpdateV4Target[];
77
92
  };
78
93
 
79
94
  export declare function checkAllFirmwareRelease(
@@ -1,15 +1,6 @@
1
- import type { IBLEFirmwareReleaseInfo } from '../settings';
2
1
  import type { Response } from '../params';
3
- import type { IDeviceBLEFirmwareStatus } from '../device';
2
+ import type { FirmwareReleaseCheckResult } from './checkAllFirmwareRelease';
4
3
 
5
- type BleFirmwareRelease = {
6
- status: IDeviceBLEFirmwareStatus;
7
- changelog: {
8
- 'zh-CN': string;
9
- 'en-US': string;
10
- }[];
11
- release: IBLEFirmwareReleaseInfo;
12
- bootloaderMode: boolean;
13
- };
14
-
15
- export declare function checkBLEFirmwareRelease(connectId?: string): Response<BleFirmwareRelease>;
4
+ export declare function checkBLEFirmwareRelease(
5
+ connectId?: string
6
+ ): Response<FirmwareReleaseCheckResult | null>;
@@ -1,12 +1,8 @@
1
1
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
2
2
  import type { CommonParams, Response } from '../params';
3
+ import type { FirmwareReleaseCheckResult } from './checkAllFirmwareRelease';
3
4
 
4
- export type CheckBootloaderReleaseResponse = {
5
- shouldUpdate: boolean;
6
- status: 'outdated' | 'valid';
7
- release: string | undefined;
8
- bootloaderMode: boolean;
9
- } | null;
5
+ export type CheckBootloaderReleaseResponse = FirmwareReleaseCheckResult | null;
10
6
 
11
7
  export type CheckBootloaderReleaseParams = {
12
8
  willUpdateFirmwareVersion?: string;