@onekeyfe/hd-core 1.2.0-alpha.162 → 1.2.0-alpha.164

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 (40) hide show
  1. package/__tests__/device-state-events.test.ts +2 -2
  2. package/__tests__/device-state-mapper.test.ts +2 -11
  3. package/__tests__/device-utils.test.ts +0 -6
  4. package/__tests__/firmware-memory-host.test.ts +126 -0
  5. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +3 -13
  6. package/__tests__/protocol-v2-resources.test.ts +2 -35
  7. package/__tests__/protocol-v2.test.ts +42 -89
  8. package/__tests__/search-devices.test.ts +3 -4
  9. package/dist/api/FirmwareUpdateV4.d.ts +1 -3
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  12. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  13. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +15 -0
  14. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  19. package/dist/index.d.ts +22 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +298 -102
  22. package/dist/protocols/protocol-v2/resources.d.ts +0 -1
  23. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  24. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  25. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/api/FirmwareUpdateV4.ts +179 -75
  28. package/src/api/SearchDevices.ts +2 -2
  29. package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
  30. package/src/api/firmware/FirmwareUpdatePlan.ts +41 -0
  31. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +6 -10
  32. package/src/core/index.ts +97 -6
  33. package/src/device/Device.ts +1 -3
  34. package/src/device/DeviceStateMapper.ts +2 -2
  35. package/src/deviceProfile/buildDeviceFeatures.ts +2 -7
  36. package/src/index.ts +6 -0
  37. package/src/protocols/protocol-v2/resources.ts +12 -16
  38. package/src/types/api/firmwareUpdate.ts +1 -1
  39. package/src/utils/deviceFeaturesCompat.ts +4 -9
  40. package/src/utils/deviceInfoUtils.ts +1 -3
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
 
@@ -954,7 +958,60 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
954
958
  * If the Bluetooth connection times out, retry up to 6 times
955
959
  * @param retryCount - Current retry count (default 0)
956
960
  */
957
- async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
961
+ // device.acquire awaits a transport reply with no deadline of its own; a
962
+ // transport that never settles (field case: Electron main lost an IPC reply,
963
+ // "reply was never sent" after 5 minutes) hangs the call forever and cancel()
964
+ // only takes effect at poll checkpoints. Race acquire against a deadline and
965
+ // the caller's abort signal so the hang is bounded and cancel is immediate.
966
+ const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
967
+
968
+ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
969
+ return new Promise<T>((resolve, reject) => {
970
+ let settled = false;
971
+ const settle = (fn: () => void) => {
972
+ if (settled) return;
973
+ settled = true;
974
+ clearTimeout(deadline);
975
+ abortSignal?.removeEventListener('abort', onAbort);
976
+ fn();
977
+ };
978
+ const onAbort = () =>
979
+ settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
980
+ const deadline = setTimeout(
981
+ () =>
982
+ settle(() =>
983
+ reject(
984
+ ERRORS.TypedError(
985
+ HardwareErrorCode.BleTimeoutError,
986
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
987
+ )
988
+ )
989
+ ),
990
+ BLE_ACQUIRE_DEADLINE_MS
991
+ );
992
+ // Attach before any early return so a late settlement of acquirePromise
993
+ // is always consumed — an abort or deadline must never leave the acquire
994
+ // rejection unhandled.
995
+ acquirePromise.then(
996
+ value => settle(() => resolve(value)),
997
+ error => settle(() => reject(error))
998
+ );
999
+ if (abortSignal) {
1000
+ if (abortSignal.aborted) {
1001
+ onAbort();
1002
+ return;
1003
+ }
1004
+ abortSignal.addEventListener('abort', onAbort);
1005
+ }
1006
+ });
1007
+ }
1008
+
1009
+ async function connectDeviceForBle(
1010
+ method: BaseMethod,
1011
+ device: Device,
1012
+ abortSignal?: AbortSignal,
1013
+ retryCount = 0
1014
+ ) {
958
1015
  try {
959
1016
  if (device.wasInterruptedByUser()) {
960
1017
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
@@ -968,9 +1025,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
968
1025
  !device.commands ||
969
1026
  device.commands.disposed;
970
1027
  if (shouldAcquire) {
971
- await device.acquire(method.payload.connectProtocol, {
972
- forceProtocolDetection: method.payload.forceProtocolDetection,
973
- });
1028
+ // The deadline/abort guards are scoped to the desktop electron
1029
+ // transport: its IPC acquire is the only path with a proven
1030
+ // never-settling failure mode, while react-native/lowlevel acquire may
1031
+ // legitimately block on a user-driven system bonding prompt for longer
1032
+ // than any sane deadline. Other envs keep the plain acquire unchanged.
1033
+ const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
1034
+ // A cancel landing during the retry backoff must not start a new acquire.
1035
+ if (useAcquireGuards && abortSignal?.aborted) {
1036
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1037
+ }
1038
+ if (!useAcquireGuards) {
1039
+ await device.acquire(method.payload.connectProtocol, {
1040
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1041
+ });
1042
+ } else {
1043
+ try {
1044
+ await raceBleAcquire(
1045
+ device.acquire(method.payload.connectProtocol, {
1046
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1047
+ }),
1048
+ abortSignal
1049
+ );
1050
+ } catch (err) {
1051
+ // A deadline hit means the transport is wedged mid-acquire; drop the
1052
+ // link before the retry so it cold-connects instead of stacking a
1053
+ // second connect onto the half-open one.
1054
+ if (
1055
+ err.errorCode === HardwareErrorCode.BleTimeoutError &&
1056
+ device.mainId &&
1057
+ device.deviceConnector
1058
+ ) {
1059
+ await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
1060
+ device.markTransportDisconnected();
1061
+ }
1062
+ throw err;
1063
+ }
1064
+ }
974
1065
  }
975
1066
  if (method.payload?.onlyConnectBleDevice) {
976
1067
  if (shouldAcquire) {
@@ -1010,7 +1101,7 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
1010
1101
  const nextRetry = retryCount + 1;
1011
1102
  Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
1012
1103
  await wait(3000);
1013
- await connectDeviceForBle(method, device, nextRetry);
1104
+ await connectDeviceForBle(method, device, abortSignal, nextRetry);
1014
1105
  } else {
1015
1106
  throw err;
1016
1107
  }
@@ -1120,7 +1211,7 @@ const ensureConnected = async (
1120
1211
  if (tryCount === 1) {
1121
1212
  device.beginConnectionAttempt();
1122
1213
  }
1123
- await connectDeviceForBle(method, device);
1214
+ await connectDeviceForBle(method, device, abortSignal);
1124
1215
  }
1125
1216
  resolve(device);
1126
1217
  return;
@@ -9,7 +9,6 @@ import {
9
9
  ERROR_CODES_REQUIRE_RELEASE,
10
10
  HardwareError,
11
11
  HardwareErrorCode,
12
- canonicalizePro2BleAdvertisementName,
13
12
  createDeferred,
14
13
  createDeviceNotSupportMethodError,
15
14
  } from '@onekeyfe/hd-shared';
@@ -673,8 +672,7 @@ export class Device extends EventEmitter {
673
672
  }
674
673
 
675
674
  getCurrentBleName() {
676
- const bleName = this.state?.identity.bleName ?? null;
677
- return bleName ? canonicalizePro2BleAdvertisementName(bleName) : null;
675
+ return this.state?.identity.bleName ?? null;
678
676
  }
679
677
 
680
678
  getCurrentLabel() {
@@ -1,4 +1,4 @@
1
- import { EFirmwareType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
1
+ import { EFirmwareType, normalizePro2FindMyAdvertisementName } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { buildProtocolV1FeaturesPayload } from '../deviceProfile/buildDeviceFeatures';
4
4
  import { resolveProtocolV2DeviceIdentity } from '../deviceProfile/protocolV2DeviceIdentity';
@@ -248,7 +248,7 @@ export const mapProtocolV2DeviceInfoToState = (
248
248
  vendor: 'onekey.so',
249
249
  serialNo: info.hw?.serial_no,
250
250
  bleName: info.coprocessor?.bt_adv_name
251
- ? canonicalizePro2BleAdvertisementName(info.coprocessor.bt_adv_name)
251
+ ? normalizePro2FindMyAdvertisementName(info.coprocessor.bt_adv_name)
252
252
  : undefined,
253
253
  deviceId: loader ? null : undefined,
254
254
  }),
@@ -1,8 +1,4 @@
1
- import {
2
- EDeviceType,
3
- EFirmwareType,
4
- canonicalizePro2BleAdvertisementName,
5
- } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
6
2
 
7
3
  import {
8
4
  resolveDeviceBleFirmwareVersion,
@@ -249,8 +245,7 @@ export const buildProtocolV2FeaturesPayload = ({
249
245
  const deviceId = status?.device_id ?? cached?.deviceId ?? null;
250
246
  const serialNo = firstValue(incomingSerialNo, cached?.serialNo) ?? '';
251
247
  const label = cached?.label ?? null;
252
- const rawBleName = firstValue(info?.coprocessor?.bt_adv_name, cached?.bleName);
253
- const bleName = rawBleName ? canonicalizePro2BleAdvertisementName(rawBleName) : rawBleName;
248
+ const bleName = firstValue(info?.coprocessor?.bt_adv_name, cached?.bleName);
254
249
  const initialized = firstValue(status?.init_states, cached?.initialized) ?? null;
255
250
  // passphrase_enabled from a locked Pro2 is not authoritative. Only DeviceStatus
256
251
  // after PIN unlock can determine the final passphrase setting.
package/src/index.ts CHANGED
@@ -21,6 +21,12 @@ 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 { prepareFirmwareUpdateV4MemoryHost } from './api/firmware/FirmwareMemoryHost';
25
+ export type {
26
+ FirmwareMemoryArtifact,
27
+ FirmwareMemoryArtifactEntry,
28
+ FirmwareUpdateV4MemoryHost,
29
+ } from './api/firmware/FirmwareMemoryHost';
24
30
  export {
25
31
  getFirmwareUpdateHostBindingGeneration,
26
32
  registerFirmwareUpdateHostBinding,
@@ -49,22 +49,10 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
49
49
  };
50
50
  }
51
51
 
52
- export function isProtocolV2ResourceArchiveEntryName(entryName: string): boolean {
53
- const normalized = entryName.replace(/\\/g, '/');
54
- if (!normalized.toLowerCase().endsWith('.okpkg')) {
55
- return false;
56
- }
57
- const parts = normalized.split('/');
58
- const fileName = parts[parts.length - 1] ?? '';
59
- return (
60
- fileName.length > 0 &&
61
- !fileName.startsWith('.') &&
62
- !parts.some(part => part === '__MACOSX' || part === '.' || part === '..' || part === '')
63
- );
64
- }
52
+ const PROTOCOL_V2_RESOURCE_DEVICE_ROOTS = ['vol0:/bundles/', 'vol0:/loaders/rom/'] as const;
65
53
 
66
- function isSafeResourceDevicePath(path: string): boolean {
67
- return !(
54
+ function isAllowedResourceDevicePath(path: string): boolean {
55
+ if (
68
56
  path.includes('\\') ||
69
57
  path.includes('//') ||
70
58
  [...path].some(char => {
@@ -72,6 +60,14 @@ function isSafeResourceDevicePath(path: string): boolean {
72
60
  return code <= 0x1f || code === 0x7f;
73
61
  }) ||
74
62
  path.split('/').some(part => part === '.' || part === '..')
63
+ ) {
64
+ return false;
65
+ }
66
+ if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH) {
67
+ return true;
68
+ }
69
+ return (
70
+ path.endsWith('.okpkg') && PROTOCOL_V2_RESOURCE_DEVICE_ROOTS.some(root => path.startsWith(root))
75
71
  );
76
72
  }
77
73
 
@@ -97,7 +93,7 @@ function readResourceDevicePath(bytes: Uint8Array): string {
97
93
  throw new Error('Invalid Pro2 RESOURCE package device path metadata');
98
94
  }
99
95
  const path = readAscii(pathBytes, 0, pathBytes.byteLength);
100
- if (!isSafeResourceDevicePath(path)) {
96
+ if (!isAllowedResourceDevicePath(path)) {
101
97
  throw new Error(`Invalid Pro2 RESOURCE package device path: ${path}`);
102
98
  }
103
99
  return path;
@@ -158,7 +158,7 @@ export interface FirmwareUpdateV4Params {
158
158
  se02Binary?: ArrayBuffer;
159
159
  se03Binary?: ArrayBuffer;
160
160
  se04Binary?: ArrayBuffer;
161
- /** Complete Protocol V2 resource ZIP. Core parses RESC packages and writes only changed files. */
161
+ /** Complete Protocol V2 resource ZIP for local development; Core converts it to a local PreparedPlan. */
162
162
  resourceArchiveBinary?: ArrayBuffer;
163
163
  forcedUpdateRes?: boolean;
164
164
  artifactReader?: FirmwareArtifactReader;
@@ -1,8 +1,4 @@
1
- import {
2
- EDeviceType,
3
- EFirmwareType,
4
- canonicalizePro2BleAdvertisementName,
5
- } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
6
2
  import { Enum_Capability } from '@onekeyfe/hd-transport';
7
3
 
8
4
  import type { PROTO } from '../constants';
@@ -129,10 +125,9 @@ export const resolveDeviceFirmwareType = (features?: DeviceFeaturesInput): EFirm
129
125
  export const resolveDeviceBleName = (features?: DeviceFeaturesInput): string | null => {
130
126
  if (!features) return null;
131
127
  const compatible = asCompatibleFeatures(features);
132
- const bleName =
133
- firstNonEmptyString(compatible.bleName, compatible.onekey_ble_name, compatible.ble_name) ??
134
- null;
135
- return bleName ? canonicalizePro2BleAdvertisementName(bleName) : null;
128
+ return (
129
+ firstNonEmptyString(compatible.bleName, compatible.onekey_ble_name, compatible.ble_name) ?? null
130
+ );
136
131
  };
137
132
 
138
133
  export const resolveDeviceFirmwareVersion = (features?: DeviceFeaturesInput): string | null => {
@@ -31,9 +31,7 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => {
31
31
  if (/^Touch/i.test(name)) return EDeviceType.Touch;
32
32
 
33
33
  const compactName = name.replace(/[\s-]/g, '');
34
- // Require a 4-hex Pro2 suffix in the compact form. A bare `^Pro2` prefix
35
- // would also match OneKey Pro names such as "Pro 22D8" / "Pro 2D8F".
36
- if (/\bPro\s*2\b/i.test(name) || /^(?:OneKey)?Pro2[a-f0-9]{4}$/i.test(compactName)) {
34
+ if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name) || /^(?:OneKey)?Pro2/i.test(compactName)) {
37
35
  return EDeviceType.Pro2;
38
36
  }
39
37
  if (/\bNeo\b/i.test(name) || /^Neo/i.test(name) || /^(?:OneKey)?Neo/i.test(compactName)) {