@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.
Files changed (57) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +12 -71
  2. package/__tests__/device-state-mapper.test.ts +16 -1
  3. package/__tests__/device-state-projector.test.ts +20 -0
  4. package/__tests__/device-state-store.test.ts +27 -0
  5. package/__tests__/homescreen.test.ts +17 -0
  6. package/__tests__/method-protocol-support.test.ts +13 -1
  7. package/__tests__/protocol-v2-resources.test.ts +113 -222
  8. package/__tests__/protocol-v2.test.ts +110 -380
  9. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV4.d.ts +0 -6
  11. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  12. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  13. package/dist/api/utils.d.ts +2 -0
  14. package/dist/api/utils.d.ts.map +1 -1
  15. package/dist/data-manager/DataManager.d.ts +1 -2
  16. package/dist/data-manager/DataManager.d.ts.map +1 -1
  17. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  18. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  19. package/dist/device/DeviceStateStore.d.ts.map +1 -1
  20. package/dist/index.d.ts +96 -33
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +515 -597
  23. package/dist/inject.d.ts.map +1 -1
  24. package/dist/protocols/protocol-v2/resources.d.ts +31 -28
  25. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  26. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -0
  27. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  28. package/dist/types/api/export.d.ts +1 -0
  29. package/dist/types/api/export.d.ts.map +1 -1
  30. package/dist/types/api/index.d.ts +2 -0
  31. package/dist/types/api/index.d.ts.map +1 -1
  32. package/dist/types/api/protocolV2ResourceManifest.d.ts +17 -0
  33. package/dist/types/api/protocolV2ResourceManifest.d.ts.map +1 -0
  34. package/dist/types/device.d.ts +8 -0
  35. package/dist/types/device.d.ts.map +1 -1
  36. package/dist/types/settings.d.ts +30 -21
  37. package/dist/types/settings.d.ts.map +1 -1
  38. package/dist/utils/homescreen.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/api/CheckAllFirmwareRelease.ts +5 -34
  41. package/src/api/FirmwareUpdateV4.ts +48 -220
  42. package/src/api/UploadPortfolio.ts +18 -0
  43. package/src/api/utils.ts +25 -0
  44. package/src/data-manager/DataManager.ts +17 -6
  45. package/src/device/DeviceStateMapper.ts +42 -0
  46. package/src/device/DeviceStateProjector.ts +58 -0
  47. package/src/device/DeviceStateStore.ts +31 -0
  48. package/src/index.ts +8 -0
  49. package/src/inject.ts +2 -0
  50. package/src/protocols/protocol-v2/resources.ts +193 -324
  51. package/src/types/api/checkAllFirmwareRelease.ts +1 -0
  52. package/src/types/api/export.ts +4 -0
  53. package/src/types/api/index.ts +2 -0
  54. package/src/types/api/protocolV2ResourceManifest.ts +19 -0
  55. package/src/types/device.ts +16 -0
  56. package/src/types/settings.ts +30 -36
  57. package/src/utils/homescreen.ts +5 -1
@@ -4,20 +4,13 @@ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
4
4
 
5
5
  import { DataManager } from '../src/data-manager';
6
6
  import {
7
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
8
- PROTOCOL_V2_RESOURCE_TYPES,
9
- buildProtocolV2ResourceUpdatePlan,
10
7
  isProtocolV2ResourceFileValid,
8
+ parseProtocolV2ResourceManifest,
11
9
  parseProtocolV2Resources,
12
- readProtocolV2ResourceInventory,
10
+ prepareProtocolV2ResourceFiles,
13
11
  } from '../src/protocols/protocol-v2/resources';
14
12
 
15
- import type {
16
- ConnectSettings,
17
- IProtocolV2BootResources,
18
- IProtocolV2Resource,
19
- RemoteConfigResponse,
20
- } from '../src/types';
13
+ import type { ConnectSettings, RemoteConfigResponse } from '../src/types';
21
14
 
22
15
  jest.mock('axios');
23
16
  jest.mock('../src/data/config', () => ({
@@ -28,45 +21,69 @@ jest.mock('../src/data/config', () => ({
28
21
  const bytesToHex = (bytes: Uint8Array) =>
29
22
  Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
30
23
 
31
- const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
32
-
33
- const createResourceHeader = (headerHash: string) => {
34
- const header = new Uint8Array(PROTOCOL_V2_OKPP_HEADER_SIZE);
35
- const view = new DataView(header.buffer);
36
- 'OKPP'.split('').forEach((char, index) => {
37
- header[index] = char.charCodeAt(0);
38
- });
39
- 'RESC'.split('').forEach((char, index) => {
40
- header[0x08 + index] = char.charCodeAt(0);
41
- });
42
- view.setUint32(0x0c, header.byteLength, true);
43
- for (let index = 0; index < headerHash.length / 2; index++) {
44
- header[0x240 + index] = Number.parseInt(headerHash.slice(index * 2, index * 2 + 2), 16);
45
- }
46
- return header;
24
+ const resourceSource = {
25
+ manifestUrl: 'https://example.com/resource/manifest.json',
47
26
  };
48
27
 
49
- const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
50
- type,
51
- url: `https://example.com/${type}.okpkg`,
52
- size: index + 100,
53
- fileHash: index.toString(16).padStart(64, '0'),
54
- headerHash: index.toString(16).padStart(128, '0'),
55
- }));
28
+ const manifestFiles = [
29
+ ['bundles/firmware_logo-build.okpkg', 'firmware_logo.okpkg', 'vol0:/bundles/firmware_logo.okpkg'],
30
+ ['bundles/font/noto-build.okpkg', 'noto.okpkg', 'vol0:/bundles/font/noto.okpkg'],
31
+ ['bundles/font/roobert-build.okpkg', 'roobert.okpkg', 'vol0:/bundles/font/roobert.okpkg'],
32
+ [
33
+ 'bundles/images/animation-build.okpkg',
34
+ 'animation.okpkg',
35
+ 'vol0:/bundles/images/animation.okpkg',
36
+ ],
37
+ ['bundles/images/images-build.okpkg', 'images.okpkg', 'vol0:/bundles/images/images.okpkg'],
38
+ [
39
+ 'bundles/images/wallpaper-build.okpkg',
40
+ 'wallpaper.okpkg',
41
+ 'vol0:/bundles/images/wallpaper.okpkg',
42
+ ],
43
+ [
44
+ 'bundles/translations/translations-build.okpkg',
45
+ 'translations.okpkg',
46
+ 'vol0:/bundles/translations/translations.okpkg',
47
+ ],
48
+ [
49
+ 'loaders/bootloader/boot_resource-build.okpkg',
50
+ 'boot_resource.okpkg',
51
+ 'vol0:/loaders/bootloader/boot_resource.okpkg',
52
+ ],
53
+ ['loaders/rom/params-build.okpkg', 'params.okpkg', 'vol0:/loaders/rom/params.okpkg'],
54
+ ].map(([archive_path, original_name, device_path], index) => {
55
+ const binary = new Uint8Array([index + 1]).buffer;
56
+ return {
57
+ archive_path,
58
+ original_name,
59
+ device_path,
60
+ size: 1,
61
+ sha256: bytesToHex(sha256(new Uint8Array(binary))),
62
+ signed: true,
63
+ sig_algo: device_path.startsWith('vol0:/bundles/') ? 'ed25519' : 'mldsa65',
64
+ payload_version: '1.0.0',
65
+ binary,
66
+ };
67
+ });
56
68
 
57
- const bootResources: IProtocolV2BootResources = {
58
- required: false,
59
- target: 'RES',
60
- manifestUrl: 'https://example.com/manifest.json',
61
- files: [
62
- {
63
- name: 'bootloader_crest.bin',
64
- url: 'https://example.com/assets/bootloader_crest.bin',
65
- devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
66
- size: 1234,
67
- fileHash: 'ab'.repeat(32),
68
- },
69
+ const resourceManifest = {
70
+ schema: 1,
71
+ artifact_name: 'pro2-resource-build',
72
+ release_name: 'resource-build',
73
+ variant: 'resource',
74
+ commit: 'a'.repeat(40),
75
+ short_sha: 'aaaaaaa',
76
+ timestamp_utc: '20260807_091424',
77
+ core_version: '1.0.0',
78
+ key_set: 'dev',
79
+ device_root: 'vol0:',
80
+ restore_mode: 'bootloader_update',
81
+ trees: [
82
+ { path: 'bundles', device: 'vol0:/bundles' },
83
+ { path: 'loaders/bootloader', device: 'vol0:/loaders/bootloader' },
84
+ { path: 'loaders/rom', device: 'vol0:/loaders/rom' },
69
85
  ],
86
+ files: manifestFiles.map(({ binary: _binary, ...file }) => file),
70
87
  };
71
88
 
72
89
  const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
@@ -85,7 +102,7 @@ const createRemoteConfig = (): RemoteConfigResponse =>
85
102
  mini: { firmware: [], ble: [] },
86
103
  touch: { firmware: [], ble: [] },
87
104
  pro: { firmware: [], ble: [] },
88
- pro2: { firmware: [], ble: [], resources: { stable: resources, boot: bootResources } },
105
+ pro2: { firmware: [], ble: [], resources: { source: resourceSource } },
89
106
  bridge: {},
90
107
  } as unknown as RemoteConfigResponse);
91
108
 
@@ -95,186 +112,51 @@ describe('Pro2 resource configuration', () => {
95
112
  DataManager.lastCheckTimestamp = 0;
96
113
  });
97
114
 
98
- test('accepts exactly seven resources and normalizes their deterministic order', () => {
99
- const parsed = parseProtocolV2Resources({
100
- stable: [...resources].reverse(),
101
- boot: bootResources,
115
+ test('accepts the manifest source and the CI schema 1 file set', () => {
116
+ expect(parseProtocolV2Resources({ source: resourceSource })).toEqual({
117
+ source: resourceSource,
102
118
  });
103
-
104
- expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
105
- expect(parsed?.boot).toEqual(bootResources);
106
- expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
107
- 'vol0:/bundles/translations/translations.okpkg'
108
- );
109
- expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.firmware_logo).toBe(
110
- 'vol0:/bundles/firmware_logo.okpkg'
111
- );
119
+ expect(parseProtocolV2ResourceManifest(resourceManifest).files).toHaveLength(9);
112
120
  });
113
121
 
114
- test('rejects incomplete, duplicate, or malformed stable sets', () => {
115
- expect(() => parseProtocolV2Resources({ stable: resources.slice(1) })).toThrow(
116
- '7 unique resource types'
117
- );
122
+ test('rejects missing source and malformed manifest paths', () => {
123
+ expect(() => parseProtocolV2Resources({})).toThrow('source is required');
118
124
  expect(() =>
119
- parseProtocolV2Resources({ stable: [...resources.slice(0, 6), resources[0]] })
120
- ).toThrow('7 unique resource types');
125
+ parseProtocolV2Resources({ source: { manifestUrl: 'http://example.com/manifest.json' } })
126
+ ).toThrow('must use HTTPS');
121
127
  expect(() =>
122
- parseProtocolV2Resources({
123
- stable: resources.map((resource, index) =>
124
- index === 0 ? { ...resource, headerHash: 'bad' } : resource
128
+ parseProtocolV2ResourceManifest({
129
+ ...resourceManifest,
130
+ files: resourceManifest.files.map((file, index) =>
131
+ index === 0 ? { ...file, archive_path: '../outside.okpkg' } : file
125
132
  ),
126
133
  })
127
- ).toThrow('headerHash');
134
+ ).toThrow('archive_path');
128
135
  });
129
136
 
130
- test('requires boot resources to remain optional and use RES file entries', () => {
131
- expect(() =>
132
- parseProtocolV2Resources({
133
- stable: resources,
134
- boot: { ...bootResources, required: true },
135
- })
136
- ).toThrow('required flag');
137
- expect(() =>
138
- parseProtocolV2Resources({
139
- stable: resources,
140
- boot: { ...bootResources, target: 'INVALID' },
141
- })
142
- ).toThrow('expected RES');
137
+ test('verifies selected manifest files and preserves manifest device paths', () => {
138
+ const prepared = prepareProtocolV2ResourceFiles({
139
+ manifest: resourceManifest,
140
+ files: manifestFiles.map(file => ({
141
+ archivePath: file.archive_path,
142
+ binary: file.binary,
143
+ })),
144
+ targetsToUpdate: ['boot_resources'],
145
+ });
146
+ expect(prepared.map(file => file.devicePath)).toEqual([
147
+ 'vol0:/loaders/bootloader/boot_resource.okpkg',
148
+ 'vol0:/loaders/rom/params.okpkg',
149
+ ]);
143
150
  expect(() =>
144
- parseProtocolV2Resources({
145
- stable: resources,
146
- boot: {
147
- ...bootResources,
148
- files: [{ ...bootResources.files[0], devicePath: '../outside.bin' }],
149
- },
151
+ prepareProtocolV2ResourceFiles({
152
+ manifest: resourceManifest,
153
+ files: manifestFiles.map((file, index) => ({
154
+ archivePath: file.archive_path,
155
+ binary: index === 0 ? new Uint8Array([0]).buffer : file.binary,
156
+ })),
157
+ targetsToUpdate: ['resource'],
150
158
  })
151
- ).toThrow('devicePath');
152
-
153
- for (const devicePath of [
154
- 'vol0:/assets/loaders\\bootloader_crest.bin',
155
- 'vol0:/assets/loaders\\\\bootloader_crest.bin',
156
- ]) {
157
- expect(() =>
158
- parseProtocolV2Resources({
159
- stable: resources,
160
- boot: {
161
- ...bootResources,
162
- files: [{ ...bootResources.files[0], devicePath }],
163
- },
164
- })
165
- ).toThrow('devicePath');
166
- }
167
- });
168
-
169
- test('downloads nothing when all resource identities match', () => {
170
- const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
171
-
172
- expect(
173
- buildProtocolV2ResourceUpdatePlan({ resources, inventory, mode: 'application' })
174
- ).toEqual({ status: 'valid', resources: [] });
175
- });
176
-
177
- test('builds inventory from existing filesystem calls without ResourceInventoryGet', async () => {
178
- const installedResources = resources.map((resource, index) => ({
179
- ...resource,
180
- size: PROTOCOL_V2_OKPP_HEADER_SIZE + index + 1,
181
- }));
182
- const resourceByPath = new Map(
183
- installedResources.map(resource => [
184
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
185
- resource,
186
- ])
187
- );
188
- const typedCall = jest.fn(
189
- (requestType: string, _responseType: string, payload: Record<string, any>) => {
190
- if (requestType === 'ResourceInventoryGet') {
191
- throw new Error('ResourceInventoryGet is unavailable on released firmware');
192
- }
193
- if (requestType === 'FilesystemPathInfoQuery') {
194
- const resource = resourceByPath.get(payload.path);
195
- return {
196
- message: {
197
- exist: Boolean(resource),
198
- directory: false,
199
- size: resource?.size ?? 0,
200
- },
201
- };
202
- }
203
- if (requestType === 'FilesystemFileRead') {
204
- const resource = resourceByPath.get(payload.file.path);
205
- if (!resource) throw new Error('missing resource');
206
- const header = createResourceHeader(resource.headerHash);
207
- const offset = Number(payload.file.offset);
208
- const chunkLength = Number(payload.chunk_len);
209
- return {
210
- message: {
211
- data: header.slice(offset, offset + chunkLength),
212
- },
213
- };
214
- }
215
- throw new Error(`Unexpected request: ${requestType}`);
216
- }
217
- );
218
-
219
- await expect(
220
- readProtocolV2ResourceInventory({
221
- commands: { typedCall },
222
- resources: installedResources,
223
- chunkSize: 4000,
224
- })
225
- ).resolves.toEqual(
226
- installedResources.map(({ type, size, headerHash }) => ({ type, size, headerHash }))
227
- );
228
- expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
229
- expect(typedCall.mock.calls.filter(call => call[0] === 'FilesystemPathInfoQuery')).toHaveLength(
230
- 7
231
- );
232
- expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
233
- });
234
-
235
- test('selects only the changed or missing resource in application mode', () => {
236
- const inventory = resources
237
- .filter(resource => resource.type !== 'noto')
238
- .map(({ type, size, headerHash }) => ({
239
- type,
240
- size: type === 'images' ? size + 1 : size,
241
- headerHash,
242
- }));
243
-
244
- const result = buildProtocolV2ResourceUpdatePlan({
245
- resources,
246
- inventory,
247
- mode: 'application',
248
- });
249
-
250
- expect(result.status).toBe('outdated');
251
- expect(result.resources.map(resource => resource.type)).toEqual(['images', 'noto']);
252
- });
253
-
254
- test('reports unknown without an application inventory and selects all in recovery mode', () => {
255
- expect(buildProtocolV2ResourceUpdatePlan({ resources, mode: 'application' })).toEqual({
256
- status: 'unknown',
257
- resources: [],
258
- });
259
- expect(
260
- buildProtocolV2ResourceUpdatePlan({ resources, mode: 'bootloader-recovery' }).resources
261
- ).toHaveLength(7);
262
- });
263
-
264
- test('uses a filesystem inventory for incremental recovery mode updates', () => {
265
- const inventory = resources.slice(1).map(({ type, size, headerHash }) => ({
266
- type,
267
- size,
268
- headerHash,
269
- }));
270
-
271
- expect(
272
- buildProtocolV2ResourceUpdatePlan({
273
- resources,
274
- inventory,
275
- mode: 'bootloader-recovery',
276
- }).resources.map(resource => resource.type)
277
- ).toEqual(['images']);
159
+ ).toThrow('verification failed');
278
160
  });
279
161
 
280
162
  test('verifies both full file size and SHA-256 before transfer', () => {
@@ -289,7 +171,7 @@ describe('Pro2 resource configuration', () => {
289
171
  );
290
172
  });
291
173
 
292
- test('applies a validated pre-release config and exposes the stable resource set', async () => {
174
+ test('applies a validated pre-release config and exposes its manifest source', async () => {
293
175
  const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
294
176
 
295
177
  await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
@@ -297,24 +179,33 @@ describe('Pro2 resource configuration', () => {
297
179
  expect(configFetcher).toHaveBeenCalledWith(
298
180
  expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
299
181
  );
300
- expect(DataManager.getProtocolV2Resources()).toEqual(resources);
301
- expect(DataManager.getProtocolV2BootResources()).toEqual(bootResources);
182
+ expect(DataManager.getProtocolV2ResourceSource()).toEqual(resourceSource);
183
+ expect(DataManager.lastCheckTimestamp).toBeGreaterThan(0);
184
+ });
185
+
186
+ test('reuses the configuration fetched during SDK initialization for an immediate update', async () => {
187
+ const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
188
+ const settings = createSettings(configFetcher);
189
+
190
+ await expect(DataManager.load(settings)).resolves.toBe(true);
191
+ await expect(DataManager.forceReloadData({ requireResources: true })).resolves.toBeUndefined();
192
+
193
+ expect(configFetcher).toHaveBeenCalledTimes(1);
302
194
  });
303
195
 
304
196
  test('keeps base SDK initialization available when remote Pro2 resources are invalid', async () => {
305
197
  const remoteConfig = createRemoteConfig();
306
- (remoteConfig.pro2 as { resources?: unknown }).resources = { stable: [] };
198
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
307
199
  const configFetcher = jest.fn().mockResolvedValue(remoteConfig);
308
200
 
309
201
  await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
310
202
 
311
- expect(DataManager.getProtocolV2Resources()).toBeUndefined();
312
- expect(DataManager.getProtocolV2BootResources()).toBeUndefined();
203
+ expect(DataManager.getProtocolV2ResourceSource()).toBeUndefined();
313
204
  });
314
205
 
315
206
  test('only blocks resource mutation when the refreshed Pro2 resources are invalid', async () => {
316
207
  const remoteConfig = createRemoteConfig();
317
- (remoteConfig.pro2 as { resources?: unknown }).resources = { stable: [] };
208
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
318
209
  const settings = createSettings(jest.fn().mockResolvedValue(remoteConfig));
319
210
  DataManager.settings = settings;
320
211