@onekeyfe/hd-core 1.2.0-alpha.40 → 1.2.0-alpha.42

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.
@@ -84,7 +84,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
84
84
  status: 'required',
85
85
  hasUpgrade: true,
86
86
  required: true,
87
- targetsToUpdate: ['boot', 'app_v1', 'resource'],
87
+ targetsToUpdate: ['boot', 'app_v1'],
88
88
  components: [
89
89
  { configKey: 'bootloader', status: 'outdated', updateTarget: 'boot' },
90
90
  { configKey: 'applicationP1', status: 'outdated', updateTarget: 'app_v1' },
@@ -247,13 +247,26 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
247
247
  firmwareVersion: '1.0.0',
248
248
  },
249
249
  getDeviceState,
250
+ getCommands: () => ({ typedCall: jest.fn().mockResolvedValue({ message: { items: [] } }) }),
250
251
  } as unknown as CheckAllFirmwareRelease['device'];
251
252
  jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
253
+ jest.spyOn(DataManager, 'getProtocolV2Resources').mockReturnValue(
254
+ ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
255
+ (type, index) => ({
256
+ type,
257
+ url: `https://example.com/${type}.okpkg`,
258
+ size: index + 1,
259
+ fileHash: 'a'.repeat(64),
260
+ headerHash: index.toString(16).padStart(128, '0'),
261
+ })
262
+ ) as any
263
+ );
252
264
 
253
265
  await expect(method.run()).resolves.toMatchObject({
254
266
  protocol: 'V2',
255
267
  deviceType: 'pro2',
256
268
  status: 'required',
269
+ resourceStatus: 'outdated',
257
270
  targetsToUpdate: ['boot', 'app_v1', 'resource'],
258
271
  firmware: {
259
272
  status: 'required',
@@ -0,0 +1,204 @@
1
+ import axios from 'axios';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+
4
+ import { DataManager } from '../src/data-manager';
5
+ import {
6
+ PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
7
+ PROTOCOL_V2_RESOURCE_TYPES,
8
+ buildProtocolV2ResourceUpdatePlan,
9
+ isProtocolV2ResourceFileValid,
10
+ parseProtocolV2ResourceInventory,
11
+ parseProtocolV2Resources,
12
+ requestProtocolV2ResourceInventory,
13
+ } from '../src/protocols/protocol-v2/resources';
14
+
15
+ import type { ConnectSettings, IProtocolV2Resource, RemoteConfigResponse } from '../src/types';
16
+
17
+ jest.mock('axios');
18
+ jest.mock('../src/data/config', () => ({
19
+ getSDKVersion: jest.fn(() => '1.0.0-test'),
20
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
21
+ }));
22
+
23
+ const bytesToHex = (bytes: Uint8Array) =>
24
+ Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
25
+
26
+ const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
27
+ type,
28
+ url: `https://example.com/${type}.okpkg`,
29
+ size: index + 100,
30
+ fileHash: index.toString(16).padStart(64, '0'),
31
+ headerHash: index.toString(16).padStart(128, '0'),
32
+ }));
33
+
34
+ const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
35
+ ({
36
+ env: 'node',
37
+ fetchConfig: true,
38
+ preRelease: true,
39
+ configFetcher,
40
+ } as ConnectSettings);
41
+
42
+ const createRemoteConfig = (): RemoteConfigResponse =>
43
+ ({
44
+ classic: { firmware: [], ble: [] },
45
+ classic1s: { firmware: [], ble: [] },
46
+ classicpure: { firmware: [], ble: [] },
47
+ mini: { firmware: [], ble: [] },
48
+ touch: { firmware: [], ble: [] },
49
+ pro: { firmware: [], ble: [] },
50
+ pro2: { firmware: [], ble: [], resources: { stable: resources } },
51
+ bridge: {},
52
+ } as unknown as RemoteConfigResponse);
53
+
54
+ describe('Pro2 resource configuration', () => {
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ DataManager.lastCheckTimestamp = 0;
58
+ });
59
+
60
+ test('accepts exactly six resources and normalizes their deterministic order', () => {
61
+ const parsed = parseProtocolV2Resources({ stable: [...resources].reverse() });
62
+
63
+ expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
64
+ expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
65
+ 'vol0:/bundles/translations/translations.okpkg'
66
+ );
67
+ });
68
+
69
+ test('rejects incomplete, duplicate, or malformed stable sets', () => {
70
+ expect(() => parseProtocolV2Resources({ stable: resources.slice(1) })).toThrow(
71
+ 'six unique resource types'
72
+ );
73
+ expect(() =>
74
+ parseProtocolV2Resources({ stable: [...resources.slice(0, 5), resources[0]] })
75
+ ).toThrow('six unique resource types');
76
+ expect(() =>
77
+ parseProtocolV2Resources({
78
+ stable: resources.map((resource, index) =>
79
+ index === 0 ? { ...resource, headerHash: 'bad' } : resource
80
+ ),
81
+ })
82
+ ).toThrow('headerHash');
83
+ });
84
+
85
+ test('downloads nothing when all resource identities match', () => {
86
+ const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
87
+
88
+ expect(
89
+ buildProtocolV2ResourceUpdatePlan({ resources, inventory, mode: 'application' })
90
+ ).toEqual({ status: 'valid', resources: [] });
91
+ });
92
+
93
+ test('normalizes the success-only ResourceInventory RPC response', async () => {
94
+ const items = resources.slice(0, 2).map((resource, index) => ({
95
+ type: index === 0 ? 'IMAGES' : 1,
96
+ size: resource.size,
97
+ header_hash: resource.headerHash,
98
+ }));
99
+ const typedCall = jest.fn().mockResolvedValue({ message: { items } });
100
+
101
+ await expect(
102
+ requestProtocolV2ResourceInventory({ commands: { typedCall } as any })
103
+ ).resolves.toEqual([
104
+ {
105
+ type: 'images',
106
+ size: resources[0].size,
107
+ headerHash: resources[0].headerHash,
108
+ },
109
+ {
110
+ type: 'animation',
111
+ size: resources[1].size,
112
+ headerHash: resources[1].headerHash,
113
+ },
114
+ ]);
115
+ expect(typedCall).toHaveBeenCalledWith(
116
+ 'ResourceInventoryGet',
117
+ 'ResourceInventory',
118
+ {},
119
+ { timeoutMs: 5000 }
120
+ );
121
+ });
122
+
123
+ test('rejects malformed or duplicate inventory identities', () => {
124
+ expect(() =>
125
+ parseProtocolV2ResourceInventory({
126
+ items: [
127
+ { type: 'IMAGES', size: 1, header_hash: 'a'.repeat(128) },
128
+ { type: 'IMAGES', size: 1, header_hash: 'b'.repeat(128) },
129
+ ],
130
+ })
131
+ ).toThrow('duplicate resource type');
132
+ expect(() =>
133
+ parseProtocolV2ResourceInventory({
134
+ items: [{ type: 'IMAGES', size: 1, header_hash: 'bad' }],
135
+ })
136
+ ).toThrow('inventory headerHash');
137
+ });
138
+
139
+ test('selects only the changed or missing resource in application mode', () => {
140
+ const inventory = resources
141
+ .filter(resource => resource.type !== 'noto')
142
+ .map(({ type, size, headerHash }) => ({
143
+ type,
144
+ size: type === 'images' ? size + 1 : size,
145
+ headerHash,
146
+ }));
147
+
148
+ const result = buildProtocolV2ResourceUpdatePlan({
149
+ resources,
150
+ inventory,
151
+ mode: 'application',
152
+ });
153
+
154
+ expect(result.status).toBe('outdated');
155
+ expect(result.resources.map(resource => resource.type)).toEqual(['images', 'noto']);
156
+ });
157
+
158
+ test('reports unknown without an application inventory and selects all in recovery mode', () => {
159
+ expect(buildProtocolV2ResourceUpdatePlan({ resources, mode: 'application' })).toEqual({
160
+ status: 'unknown',
161
+ resources: [],
162
+ });
163
+ expect(
164
+ buildProtocolV2ResourceUpdatePlan({ resources, mode: 'bootloader-recovery' }).resources
165
+ ).toHaveLength(6);
166
+ });
167
+
168
+ test('verifies both full file size and SHA-256 before transfer', () => {
169
+ const bytes = new Uint8Array([1, 2, 3]);
170
+ const binary = bytes.buffer;
171
+ const identity = { size: bytes.byteLength, fileHash: bytesToHex(sha256(bytes)) };
172
+
173
+ expect(isProtocolV2ResourceFileValid(binary, identity)).toBe(true);
174
+ expect(isProtocolV2ResourceFileValid(binary, { ...identity, size: 4 })).toBe(false);
175
+ expect(isProtocolV2ResourceFileValid(binary, { ...identity, fileHash: '0'.repeat(64) })).toBe(
176
+ false
177
+ );
178
+ });
179
+
180
+ test('applies a validated pre-release config and exposes the stable resource set', async () => {
181
+ const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
182
+
183
+ await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
184
+
185
+ expect(configFetcher).toHaveBeenCalledWith(
186
+ expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
187
+ );
188
+ expect(DataManager.getProtocolV2Resources()).toEqual(resources);
189
+ });
190
+
191
+ test('does not advance the cache timestamp when refresh fails', async () => {
192
+ jest.spyOn(axios, 'get').mockRejectedValue(new Error('offline'));
193
+ const settings = createSettings(jest.fn().mockResolvedValue(null));
194
+ DataManager.settings = settings;
195
+
196
+ await DataManager.checkAndReloadData();
197
+
198
+ expect(DataManager.lastCheckTimestamp).toBe(0);
199
+ await expect(DataManager.forceReloadData()).rejects.toThrow(
200
+ 'Unable to refresh the latest remote config'
201
+ );
202
+ expect(DataManager.lastCheckTimestamp).toBe(0);
203
+ });
204
+ });