@onekeyfe/hd-core 1.2.0-alpha.145 → 1.2.0-alpha.147

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/src/core/index.ts CHANGED
@@ -415,6 +415,10 @@ const onCallDevice = async (
415
415
  if (method.payload?.onlyConnectBleDevice) {
416
416
  preWarmCallbackTask?.resolve();
417
417
  Log.debug('Call API - only connect ble device: ', device?.mainId);
418
+ // This early return bypasses the normal-path releaseTask at the end of the
419
+ // call; without it the task leaks and haunts every later queue snapshot
420
+ // and cancel sweep (field log: a completed task lingered for 6 minutes).
421
+ requestQueue.releaseTask(method.responseID);
418
422
  return createResponseMessage(method.responseID, true, null);
419
423
  }
420
424
 
@@ -914,13 +918,10 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
914
918
  return true;
915
919
  }
916
920
 
917
- export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
918
- const typedError = error as { errorCode?: unknown; message?: unknown };
919
- const message =
920
- typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
921
+ function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
922
+ const message = error instanceof Error ? error.message : String(error ?? '');
921
923
  return (
922
924
  method.payload.connectProtocol === 'V2' &&
923
- typedError?.errorCode === HardwareErrorCode.RuntimeError &&
924
925
  message.includes('Device protocol mismatch') &&
925
926
  message.includes('expected V2') &&
926
927
  message.includes('did not respond to expected protocol')
@@ -941,7 +942,60 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
941
942
  * If the Bluetooth connection times out, retry up to 6 times
942
943
  * @param retryCount - Current retry count (default 0)
943
944
  */
944
- async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
945
+ // device.acquire awaits a transport reply with no deadline of its own; a
946
+ // transport that never settles (field case: Electron main lost an IPC reply,
947
+ // "reply was never sent" after 5 minutes) hangs the call forever and cancel()
948
+ // only takes effect at poll checkpoints. Race acquire against a deadline and
949
+ // the caller's abort signal so the hang is bounded and cancel is immediate.
950
+ const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
951
+
952
+ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
953
+ return new Promise<T>((resolve, reject) => {
954
+ let settled = false;
955
+ const settle = (fn: () => void) => {
956
+ if (settled) return;
957
+ settled = true;
958
+ clearTimeout(deadline);
959
+ abortSignal?.removeEventListener('abort', onAbort);
960
+ fn();
961
+ };
962
+ const onAbort = () =>
963
+ settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
964
+ const deadline = setTimeout(
965
+ () =>
966
+ settle(() =>
967
+ reject(
968
+ ERRORS.TypedError(
969
+ HardwareErrorCode.BleTimeoutError,
970
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
971
+ )
972
+ )
973
+ ),
974
+ BLE_ACQUIRE_DEADLINE_MS
975
+ );
976
+ // Attach before any early return so a late settlement of acquirePromise
977
+ // is always consumed — an abort or deadline must never leave the acquire
978
+ // rejection unhandled.
979
+ acquirePromise.then(
980
+ value => settle(() => resolve(value)),
981
+ error => settle(() => reject(error))
982
+ );
983
+ if (abortSignal) {
984
+ if (abortSignal.aborted) {
985
+ onAbort();
986
+ return;
987
+ }
988
+ abortSignal.addEventListener('abort', onAbort);
989
+ }
990
+ });
991
+ }
992
+
993
+ async function connectDeviceForBle(
994
+ method: BaseMethod,
995
+ device: Device,
996
+ abortSignal?: AbortSignal,
997
+ retryCount = 0
998
+ ) {
945
999
  try {
946
1000
  if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
947
1001
  await device.release();
@@ -952,9 +1006,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
952
1006
  !device.commands ||
953
1007
  device.commands.disposed;
954
1008
  if (shouldAcquire) {
955
- await device.acquire(method.payload.connectProtocol, {
956
- forceProtocolDetection: method.payload.forceProtocolDetection,
957
- });
1009
+ // The deadline/abort guards are scoped to the desktop electron
1010
+ // transport: its IPC acquire is the only path with a proven
1011
+ // never-settling failure mode, while react-native/lowlevel acquire may
1012
+ // legitimately block on a user-driven system bonding prompt for longer
1013
+ // than any sane deadline. Other envs keep the plain acquire unchanged.
1014
+ const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
1015
+ // A cancel landing during the retry backoff must not start a new acquire.
1016
+ if (useAcquireGuards && abortSignal?.aborted) {
1017
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1018
+ }
1019
+ if (!useAcquireGuards) {
1020
+ await device.acquire(method.payload.connectProtocol, {
1021
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1022
+ });
1023
+ } else {
1024
+ try {
1025
+ await raceBleAcquire(
1026
+ device.acquire(method.payload.connectProtocol, {
1027
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1028
+ }),
1029
+ abortSignal
1030
+ );
1031
+ } catch (err) {
1032
+ // A deadline hit means the transport is wedged mid-acquire; drop the
1033
+ // link before the retry so it cold-connects instead of stacking a
1034
+ // second connect onto the half-open one.
1035
+ if (
1036
+ err.errorCode === HardwareErrorCode.BleTimeoutError &&
1037
+ device.mainId &&
1038
+ device.deviceConnector
1039
+ ) {
1040
+ await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
1041
+ device.markTransportDisconnected();
1042
+ }
1043
+ throw err;
1044
+ }
1045
+ }
958
1046
  }
959
1047
  if (method.payload?.onlyConnectBleDevice) {
960
1048
  if (shouldAcquire) {
@@ -1000,7 +1088,7 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
1000
1088
  const nextRetry = retryCount + 1;
1001
1089
  Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
1002
1090
  await wait(3000);
1003
- await connectDeviceForBle(method, device, nextRetry);
1091
+ await connectDeviceForBle(method, device, abortSignal, nextRetry);
1004
1092
  } else {
1005
1093
  throw err;
1006
1094
  }
@@ -1105,7 +1193,7 @@ const ensureConnected = async (
1105
1193
  if (abort()) {
1106
1194
  return;
1107
1195
  }
1108
- await connectDeviceForBle(method, device);
1196
+ await connectDeviceForBle(method, device, abortSignal);
1109
1197
  }
1110
1198
  resolve(device);
1111
1199
  return;
package/src/index.ts CHANGED
@@ -21,6 +21,10 @@ export { executeCallback, cleanupCallback };
21
21
  export { preloadSessionCache } from './device/Device';
22
22
  export { projectFeatures as projectDeviceStateFeatures } from './device/DeviceStateProjector';
23
23
  export { getMethodSupportedProtocols } from './api/utils';
24
+ export {
25
+ parseProtocolV2ResourceManifest,
26
+ selectProtocolV2ResourceManifestFiles,
27
+ } from './protocols/protocol-v2/resources';
24
28
  export { prepareFirmwareUpdateV4MemoryHost } from './api/firmware/FirmwareMemoryHost';
25
29
  export type {
26
30
  FirmwareMemoryArtifact,
@@ -1,20 +1,29 @@
1
- import { bytesToHex } from '@noble/hashes/utils';
2
-
3
- import type { IProtocolV2Resources, IVersionArray } from '../../types';
1
+ import type {
2
+ IProtocolV2ResourceManifest,
3
+ IProtocolV2ResourceManifestFile,
4
+ IProtocolV2Resources,
5
+ } from '../../types';
6
+ import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
4
7
 
5
8
  export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH =
6
9
  'vol0:/loaders/bootloader/boot_resource.okpkg';
7
10
  export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
8
11
  export const PROTOCOL_V2_ROM_PARAMS_PACKAGE_PATH = 'vol0:/loaders/rom/params.okpkg';
9
- export const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE = 0x5f90;
10
12
 
11
- const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION = 1;
12
- const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET = 0x6c;
13
- const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE = 64;
14
- const PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET = 0x200;
15
- const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET = 0x240;
16
- const PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE = 64;
17
- const PROTOCOL_V2_RESOURCE_PACKAGE_TYPE = 'RESC';
13
+ const SHA256_HEX_LENGTH = 64;
14
+
15
+ function normalizeHex(value: unknown, expectedLength: number, field: string): string {
16
+ if (typeof value !== 'string') {
17
+ throw new Error(`Invalid Pro2 resource ${field}: expected a hexadecimal string`);
18
+ }
19
+ const normalized = value.replace(/^0x/i, '').toLowerCase();
20
+ if (normalized.length !== expectedLength || !/^[0-9a-f]+$/.test(normalized)) {
21
+ throw new Error(
22
+ `Invalid Pro2 resource ${field}: expected ${expectedLength} hexadecimal characters`
23
+ );
24
+ }
25
+ return normalized;
26
+ }
18
27
 
19
28
  /** Validate a complete Pro2 stable resource set from remote configuration. */
20
29
  export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
@@ -49,113 +58,114 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
49
58
  };
50
59
  }
51
60
 
52
- const PROTOCOL_V2_RESOURCE_DEVICE_ROOTS = ['vol0:/bundles/', 'vol0:/loaders/rom/'] as const;
61
+ const PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS = [
62
+ 'vol0:/bundles/',
63
+ 'vol0:/loaders/rom/',
64
+ ] as const;
53
65
 
54
- function isAllowedResourceDevicePath(path: string): boolean {
66
+ function isAllowedManifestDevicePath(path: string): boolean {
55
67
  if (
68
+ !path.endsWith('.okpkg') ||
56
69
  path.includes('\\') ||
57
70
  path.includes('//') ||
58
- [...path].some(char => {
59
- const code = char.charCodeAt(0);
60
- return code <= 0x1f || code === 0x7f;
61
- }) ||
62
71
  path.split('/').some(part => part === '.' || part === '..')
63
72
  ) {
64
73
  return false;
65
74
  }
66
- if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH) {
75
+ if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) {
67
76
  return true;
68
77
  }
69
- return (
70
- path.endsWith('.okpkg') && PROTOCOL_V2_RESOURCE_DEVICE_ROOTS.some(root => path.startsWith(root))
71
- );
78
+ return PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS.some(root => path.startsWith(root));
72
79
  }
73
80
 
74
- function readAscii(bytes: Uint8Array, offset: number, length: number): string {
75
- return Array.from(bytes.slice(offset, offset + length))
76
- .map(byte => String.fromCharCode(byte))
77
- .join('');
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}`);
84
+ }
85
+ return value;
78
86
  }
79
87
 
80
- function readResourceDevicePath(bytes: Uint8Array): string {
81
- const metadata = bytes.slice(
82
- PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET,
83
- PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE
84
- );
85
- const terminator = metadata.indexOf(0);
86
- const pathBytes = terminator === -1 ? metadata : metadata.slice(0, terminator);
87
- const padding = terminator === -1 ? new Uint8Array(0) : metadata.slice(terminator);
88
+ function assertManifestRelativePath(value: unknown, field: string): string {
89
+ const path = assertManifestString(value, field);
88
90
  if (
89
- pathBytes.byteLength === 0 ||
90
- Array.from(pathBytes).some(byte => byte < 0x20 || byte > 0x7e) ||
91
- Array.from(padding).some(byte => byte !== 0)
91
+ path.startsWith('/') ||
92
+ path.includes('\\') ||
93
+ path.includes(':') ||
94
+ path.split('/').some(part => !part || part === '.' || part === '..')
92
95
  ) {
93
- throw new Error('Invalid Pro2 RESOURCE package device path metadata');
94
- }
95
- const path = readAscii(pathBytes, 0, pathBytes.byteLength);
96
- if (!isAllowedResourceDevicePath(path)) {
97
- throw new Error(`Invalid Pro2 RESOURCE package device path: ${path}`);
96
+ throw new Error(`Invalid Pro2 resource manifest ${field}`);
98
97
  }
99
98
  return path;
100
99
  }
101
100
 
102
- export type ProtocolV2ResourcePackageHeader = {
103
- version: IVersionArray;
104
- payloadLength: number;
105
- devicePath: string;
106
- payloadHash: string;
107
- headerHash: string;
108
- };
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 =
111
+ file.original_name === undefined
112
+ ? archivePath.split('/').pop() ?? archivePath
113
+ : assertManifestRelativePath(file.original_name, `files[${index}].original_name`);
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`);
123
+ }
124
+ const digest = normalizeHex(file.sha256, SHA256_HEX_LENGTH, `files[${index}].sha256`);
125
+ if (!archivePath.endsWith('.okpkg') || !originalName.endsWith('.okpkg')) {
126
+ throw new Error(`Invalid Pro2 resource manifest files[${index}] package extension`);
127
+ }
128
+ return {
129
+ archive_path: archivePath,
130
+ original_name: originalName,
131
+ device_path: devicePath,
132
+ size: Number(file.size),
133
+ sha256: digest,
134
+ ...(file.signed === undefined ? {} : { signed: file.signed }),
135
+ ...(file.sig_algo === undefined ? {} : { sig_algo: file.sig_algo }),
136
+ ...(file.payload_version === undefined ? {} : { payload_version: file.payload_version }),
137
+ };
138
+ }
109
139
 
110
- export function parseProtocolV2ResourcePackageHeader(
111
- bytes: Uint8Array,
112
- packageSize: number
113
- ): ProtocolV2ResourcePackageHeader {
114
- if (bytes.byteLength < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
115
- throw new Error('Pro2 RESOURCE package is shorter than its header');
116
- }
117
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
118
- const headerVersion = view.getUint32(0x04, true);
119
- const headerLength = view.getUint32(0x0c, true);
120
- const payloadLength = view.getUint32(0x14, true);
140
+ export function parseProtocolV2ResourceManifest(value: unknown): IProtocolV2ResourceManifest {
141
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
142
+ throw new Error('Invalid Pro2 resource manifest');
143
+ }
144
+ const manifest = value as Partial<IProtocolV2ResourceManifest>;
145
+ if (!Array.isArray(manifest.files)) {
146
+ throw new Error('Invalid Pro2 resource manifest files');
147
+ }
148
+ const files = manifest.files.map(parseProtocolV2ResourceManifestFile);
149
+ const devicePaths = new Set(files.map(file => file.device_path));
150
+ const archivePaths = new Set(files.map(file => file.archive_path));
121
151
  if (
122
- readAscii(bytes, 0, 4) !== 'OKPP' ||
123
- readAscii(bytes, 0x08, 4) !== PROTOCOL_V2_RESOURCE_PACKAGE_TYPE ||
124
- headerVersion !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION ||
125
- headerLength !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
126
- payloadLength <= 0 ||
127
- headerLength + payloadLength !== packageSize
152
+ files.length === 0 ||
153
+ devicePaths.size !== files.length ||
154
+ archivePaths.size !== files.length
128
155
  ) {
129
- throw new Error('Invalid Pro2 RESOURCE package header');
156
+ throw new Error('Invalid Pro2 resource manifest file set');
130
157
  }
131
-
132
- const packedVersion = view.getUint32(0x10, true);
133
158
  return {
134
- version: [
135
- Math.floor(packedVersion / 0x10000) % 0x100,
136
- Math.floor(packedVersion / 0x100) % 0x100,
137
- packedVersion % 0x100,
138
- ],
139
- payloadLength,
140
- devicePath: readResourceDevicePath(bytes),
141
- payloadHash: bytesToHex(
142
- bytes.slice(
143
- PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET,
144
- PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
145
- )
146
- ),
147
- headerHash: bytesToHex(
148
- bytes.slice(
149
- PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET,
150
- PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
151
- )
152
- ),
159
+ files,
153
160
  };
154
161
  }
155
162
 
156
- export function parseProtocolV2ResourcePackage(
157
- binary: ArrayBuffer | Uint8Array
158
- ): ProtocolV2ResourcePackageHeader {
159
- const bytes = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
160
- return parseProtocolV2ResourcePackageHeader(bytes, bytes.byteLength);
163
+ export function selectProtocolV2ResourceManifestFiles({
164
+ manifest,
165
+ targetsToUpdate,
166
+ }: {
167
+ manifest: IProtocolV2ResourceManifest;
168
+ targetsToUpdate: readonly FirmwareUpdateV4Target[];
169
+ }): IProtocolV2ResourceManifestFile[] {
170
+ return targetsToUpdate.includes('resource') ? [...manifest.files] : [];
161
171
  }
@@ -74,6 +74,21 @@ export type IProtocolV2Resources = {
74
74
  source: IProtocolV2ResourceSource;
75
75
  };
76
76
 
77
+ export type IProtocolV2ResourceManifestFile = {
78
+ archive_path: string;
79
+ original_name?: string;
80
+ device_path: string;
81
+ size: number;
82
+ sha256: string;
83
+ signed?: boolean;
84
+ sig_algo?: string;
85
+ payload_version?: string | null;
86
+ };
87
+
88
+ export type IProtocolV2ResourceManifest = {
89
+ files: IProtocolV2ResourceManifestFile[];
90
+ };
91
+
77
92
  /** STM32 firmware config */
78
93
  export type IFirmwareReleaseInfo = {
79
94
  required: boolean;
@@ -30,13 +30,8 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => {
30
30
  if (/^T/i.test(name)) return EDeviceType.Touch;
31
31
  if (/^Touch/i.test(name)) return EDeviceType.Touch;
32
32
 
33
- const compactName = name.replace(/[\s-]/g, '');
34
- if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name) || /^(?:OneKey)?Pro2/i.test(compactName)) {
35
- return EDeviceType.Pro2;
36
- }
37
- if (/\bNeo\b/i.test(name) || /^Neo/i.test(name) || /^(?:OneKey)?Neo/i.test(compactName)) {
38
- return EDeviceType.Neo;
39
- }
33
+ if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name)) return EDeviceType.Pro2;
34
+ if (/\bNeo\b/i.test(name) || /^Neo/i.test(name)) return EDeviceType.Neo;
40
35
  if (/\bPro\b/i.test(name) || /^Pro/i.test(name)) return EDeviceType.Pro;
41
36
 
42
37
  return EDeviceType.Unknown;