@onekeyfe/hd-core 1.2.0-alpha.77 → 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.
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +12 -71
- package/__tests__/device-state-mapper.test.ts +16 -1
- package/__tests__/device-state-projector.test.ts +20 -0
- package/__tests__/device-state-store.test.ts +27 -0
- package/__tests__/homescreen.test.ts +17 -0
- package/__tests__/method-protocol-support.test.ts +13 -1
- package/__tests__/protocol-v2-resources.test.ts +113 -222
- package/__tests__/protocol-v2.test.ts +110 -380
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +0 -6
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/utils.d.ts +2 -0
- package/dist/api/utils.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -2
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/device/DeviceStateStore.d.ts.map +1 -1
- package/dist/index.d.ts +96 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +515 -597
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +31 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2ResourceManifest.d.ts +17 -0
- package/dist/types/api/protocolV2ResourceManifest.d.ts.map +1 -0
- package/dist/types/device.d.ts +8 -0
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/settings.d.ts +30 -21
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/homescreen.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +5 -34
- package/src/api/FirmwareUpdateV4.ts +48 -220
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/utils.ts +25 -0
- package/src/data-manager/DataManager.ts +17 -6
- package/src/device/DeviceStateMapper.ts +42 -0
- package/src/device/DeviceStateProjector.ts +58 -0
- package/src/device/DeviceStateStore.ts +31 -0
- package/src/index.ts +8 -0
- package/src/inject.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +193 -324
- package/src/types/api/checkAllFirmwareRelease.ts +1 -0
- package/src/types/api/export.ts +4 -0
- package/src/types/api/index.ts +2 -0
- package/src/types/api/protocolV2ResourceManifest.ts +19 -0
- package/src/types/device.ts +16 -0
- package/src/types/settings.ts +30 -36
- package/src/utils/homescreen.ts +5 -1
package/src/index.ts
CHANGED
|
@@ -19,6 +19,14 @@ export * from './types';
|
|
|
19
19
|
export { whitelist, whitelistExtension } from './data/config';
|
|
20
20
|
export { executeCallback, cleanupCallback };
|
|
21
21
|
export { preloadSessionCache } from './device/Device';
|
|
22
|
+
export { projectFeatures as projectDeviceStateFeatures } from './device/DeviceStateProjector';
|
|
23
|
+
export { getMethodSupportedProtocols } from './api/utils';
|
|
24
|
+
export {
|
|
25
|
+
parseProtocolV2ResourceManifest,
|
|
26
|
+
prepareProtocolV2ResourceFiles,
|
|
27
|
+
resolveProtocolV2ResourceManifestFileUrl,
|
|
28
|
+
selectProtocolV2ResourceManifestFiles,
|
|
29
|
+
} from './protocols/protocol-v2/resources';
|
|
22
30
|
export {
|
|
23
31
|
getFirmwareUpdateHostBindingGeneration,
|
|
24
32
|
registerFirmwareUpdateHostBinding,
|
package/src/inject.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
prepareFirmwareUpdatePlan,
|
|
4
4
|
validateFirmwareUpdatePreparedPlan,
|
|
5
5
|
} from './api/firmware/FirmwareUpdatePreparedPlan';
|
|
6
|
+
import { prepareProtocolV2ResourceFiles } from './protocols/protocol-v2/resources';
|
|
6
7
|
import {
|
|
7
8
|
getFirmwareUpdateHostBindingGeneration,
|
|
8
9
|
registerFirmwareUpdateHostBinding,
|
|
@@ -168,6 +169,7 @@ export const createCoreApi = (
|
|
|
168
169
|
getFirmwareUpdateHostBindingGeneration,
|
|
169
170
|
prepareFirmwareUpdatePlan,
|
|
170
171
|
validateFirmwareUpdatePreparedPlan,
|
|
172
|
+
prepareProtocolV2ResourceFiles,
|
|
171
173
|
getLogs: () => call({ method: 'getLogs' }),
|
|
172
174
|
clearSessionCache: params => call({ ...params, method: 'clearSessionCache' }),
|
|
173
175
|
/**
|
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
IProtocolV2ResourceFile,
|
|
8
|
-
IProtocolV2ResourceType,
|
|
4
|
+
IProtocolV2ResourceManifest,
|
|
5
|
+
IProtocolV2ResourceManifestFile,
|
|
9
6
|
IProtocolV2Resources,
|
|
10
7
|
} from '../../types';
|
|
11
|
-
import type {
|
|
8
|
+
import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
|
|
12
9
|
|
|
13
|
-
export const
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
27
|
+
return normalized;
|
|
74
28
|
}
|
|
75
29
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (value
|
|
79
|
-
if (
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
49
|
+
const PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS = [
|
|
50
|
+
'vol0:/bundles/',
|
|
51
|
+
'vol0:/loaders/rom/',
|
|
52
|
+
] as const;
|
|
106
53
|
|
|
107
|
-
|
|
54
|
+
function isAllowedManifestDevicePath(path: string): boolean {
|
|
108
55
|
if (
|
|
109
|
-
|
|
56
|
+
!path.endsWith('.okpkg') ||
|
|
57
|
+
path.includes('\\') ||
|
|
58
|
+
path.includes('//') ||
|
|
59
|
+
path.split('/').some(part => part === '.' || part === '..')
|
|
110
60
|
) {
|
|
111
|
-
return
|
|
61
|
+
return false;
|
|
112
62
|
}
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
86
|
+
return path;
|
|
207
87
|
}
|
|
208
88
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!
|
|
224
|
-
throw new Error(`Invalid Pro2 resource
|
|
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
|
|
227
|
-
if (
|
|
228
|
-
throw new Error(`Invalid Pro2 resource
|
|
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 (
|
|
231
|
-
throw new Error(`Invalid Pro2 resource
|
|
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 (
|
|
234
|
-
throw new Error(`Invalid Pro2 resource
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
246
|
-
if (!value || typeof value !== 'object') {
|
|
247
|
-
throw new Error('Invalid Pro2
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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 =
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
mode,
|
|
345
|
-
forced = false,
|
|
220
|
+
export function prepareProtocolV2ResourceFiles({
|
|
221
|
+
manifest: value,
|
|
222
|
+
files,
|
|
223
|
+
targetsToUpdate,
|
|
346
224
|
}: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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:
|
|
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[];
|
package/src/types/api/export.ts
CHANGED
|
@@ -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 {
|
package/src/types/api/index.ts
CHANGED
|
@@ -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
|
/**
|