@onekeyfe/hardware-cli 1.2.0-alpha.9 → 1.2.0-alpha.90

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.
@@ -0,0 +1,3 @@
1
+ export declare function selectSearchDevice<T extends {
2
+ connectId?: string | null;
3
+ }>(devices: T[], preferredConnectId?: string): T | undefined;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectSearchDevice = void 0;
4
+ function selectSearchDevice(devices, preferredConnectId) {
5
+ if (preferredConnectId) {
6
+ return (devices.find(device => device.connectId === preferredConnectId) ??
7
+ { connectId: preferredConnectId });
8
+ }
9
+ return devices[0];
10
+ }
11
+ exports.selectSearchDevice = selectSearchDevice;
@@ -0,0 +1,19 @@
1
+ import type { CoreApi, DeviceStateScope } from '@onekeyfe/hd-core';
2
+ type DeviceStateSdk = Pick<CoreApi, 'searchDevices' | 'getDeviceState' | 'getFeatures'>;
3
+ /**
4
+ * Unified state entry for the new CLI. Resolve the user-facing connectId through
5
+ * discovery so V1 serial IDs map to the process-local USB path.
6
+ */
7
+ export declare const getCanonicalDeviceState: (sdk: DeviceStateSdk, connectId: string | undefined, scope: DeviceStateScope) => Promise<import("@onekeyfe/hd-core").Unsuccessful | {
8
+ success: true;
9
+ payload: import("@onekeyfe/hd-core").DeviceState;
10
+ }>;
11
+ /**
12
+ * Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
13
+ * Public SDK.getFeatures remains V1-only.
14
+ */
15
+ export declare const getCompatibleFeatures: (sdk: DeviceStateSdk, connectId?: string) => Promise<import("@onekeyfe/hd-core").Unsuccessful | {
16
+ success: true;
17
+ payload: import("@onekeyfe/hd-core").Features;
18
+ }>;
19
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCompatibleFeatures = exports.getCanonicalDeviceState = void 0;
4
+ const createDeviceNotFoundResult = (connectId) => ({
5
+ success: false,
6
+ payload: {
7
+ code: 'DEVICE_NOT_FOUND',
8
+ error: connectId ? `Device not found: ${connectId}` : 'No device found',
9
+ },
10
+ });
11
+ const resolveSearchDevice = async (sdk, connectId) => {
12
+ const searchResult = await sdk.searchDevices();
13
+ if (!searchResult.success)
14
+ return searchResult;
15
+ const devices = searchResult.payload;
16
+ const device = connectId ? devices.find(item => item.connectId === connectId) : devices[0];
17
+ if (!device?.connectId)
18
+ return createDeviceNotFoundResult(connectId);
19
+ return { success: true, payload: device };
20
+ };
21
+ /**
22
+ * Unified state entry for the new CLI. Resolve the user-facing connectId through
23
+ * discovery so V1 serial IDs map to the process-local USB path.
24
+ */
25
+ const getCanonicalDeviceState = async (sdk, connectId, scope) => {
26
+ const deviceResult = await resolveSearchDevice(sdk, connectId);
27
+ if (!deviceResult.success)
28
+ return deviceResult;
29
+ if (scope === 'runtime' && deviceResult.payload.state) {
30
+ return { success: true, payload: deviceResult.payload.state };
31
+ }
32
+ const resolvedConnectId = deviceResult.payload.connectId ?? undefined;
33
+ if (!resolvedConnectId)
34
+ return createDeviceNotFoundResult();
35
+ return sdk.getDeviceState(resolvedConnectId, { scope });
36
+ };
37
+ exports.getCanonicalDeviceState = getCanonicalDeviceState;
38
+ /**
39
+ * Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
40
+ * Public SDK.getFeatures remains V1-only.
41
+ */
42
+ const getCompatibleFeatures = async (sdk, connectId) => {
43
+ const deviceResult = await resolveSearchDevice(sdk, connectId);
44
+ if (!deviceResult.success)
45
+ return deviceResult;
46
+ const device = deviceResult.payload;
47
+ const protocol = device.state?.protocol ?? device.features?.protocol;
48
+ if (protocol === 'V2') {
49
+ if (!device.features) {
50
+ return {
51
+ success: false,
52
+ payload: {
53
+ code: 'DEVICE_FEATURES_UNAVAILABLE',
54
+ error: 'Protocol V2 compatibility features are unavailable',
55
+ },
56
+ };
57
+ }
58
+ return { success: true, payload: device.features };
59
+ }
60
+ return sdk.getFeatures(device.connectId ?? '');
61
+ };
62
+ exports.getCompatibleFeatures = getCompatibleFeatures;
@@ -20,5 +20,6 @@ export declare function findPinentry(): string | null;
20
20
  export interface PinentryResult {
21
21
  value: string;
22
22
  passphraseOnDevice: boolean;
23
+ attachPinOnDevice?: boolean;
23
24
  }
24
25
  export declare function promptPassphraseViaPinentry(): Promise<PinentryResult>;
package/dist/sdk.d.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  * Passphrase flow aligns with app-monorepo CLI:
6
6
  * - Standard wallet: --use-empty-passphrase, auto-respond
7
7
  * - Hidden wallet: interactive 1/2/3 selection (standard / pinentry / on-device)
8
- * - Session caching: passphraseState + sessionId stored in OS keychain,
9
- * preloaded via preloadSessionCache on next invocation
8
+ * - Legacy session caching: existing OS keychain entries may still be preloaded,
9
+ * but public SDK responses never expose new device session ids
10
10
  */
11
11
  import HardwareSDK from '@onekeyfe/hd-common-connect-sdk';
12
+ import type { PinentryResult } from './pinentry';
12
13
  export interface SDKOptions {
13
14
  connectId?: string;
14
15
  passphraseState?: string;
@@ -16,6 +17,13 @@ export interface SDKOptions {
16
17
  debug?: boolean;
17
18
  transport?: 'usb' | 'ble';
18
19
  }
20
+ /**
21
+ * Prompt user to select wallet type (aligns with app-monorepo flow):
22
+ * 1. Standard wallet (no passphrase)
23
+ * 2. Hidden wallet — enter passphrase via pinentry (secure OS dialog)
24
+ * 3. Hidden wallet — enter passphrase on device screen
25
+ */
26
+ export declare function resolvePassphraseByChoice(choice: '1' | '2' | '3' | '4'): Promise<PinentryResult>;
19
27
  export declare function createSDK(opts: SDKOptions): Promise<typeof HardwareSDK>;
20
28
  /**
21
29
  * Release the SDK and clear the singleton. Must be called before the CLI
package/dist/sdk.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Passphrase flow aligns with app-monorepo CLI:
7
7
  * - Standard wallet: --use-empty-passphrase, auto-respond
8
8
  * - Hidden wallet: interactive 1/2/3 selection (standard / pinentry / on-device)
9
- * - Session caching: passphraseState + sessionId stored in OS keychain,
10
- * preloaded via preloadSessionCache on next invocation
9
+ * - Legacy session caching: existing OS keychain entries may still be preloaded,
10
+ * but public SDK responses never expose new device session ids
11
11
  */
12
12
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
13
  if (k2 === undefined) k2 = k;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.disposeSDK = exports.createSDK = void 0;
39
+ exports.disposeSDK = exports.createSDK = exports.resolvePassphraseByChoice = void 0;
40
40
  const readline = __importStar(require("node:readline"));
41
41
  const hd_common_connect_sdk_1 = __importDefault(require("@onekeyfe/hd-common-connect-sdk"));
42
42
  const hd_core_1 = require("@onekeyfe/hd-core");
@@ -76,8 +76,15 @@ function resolvePassphraseByChoice(choice) {
76
76
  return Promise.resolve({ value: '', passphraseOnDevice: false });
77
77
  if (choice === '2')
78
78
  return (0, pinentry_1.promptPassphraseViaPinentry)();
79
+ if (choice === '4')
80
+ return Promise.resolve({
81
+ value: '',
82
+ passphraseOnDevice: false,
83
+ attachPinOnDevice: true,
84
+ });
79
85
  return Promise.resolve({ value: '', passphraseOnDevice: true });
80
86
  }
87
+ exports.resolvePassphraseByChoice = resolvePassphraseByChoice;
81
88
  function promptPassphraseMode() {
82
89
  if (!process.stdin.isTTY) {
83
90
  return Promise.resolve({ value: '', passphraseOnDevice: true });
@@ -94,16 +101,17 @@ function promptPassphraseMode() {
94
101
  ' 1. Standard wallet (no passphrase)',
95
102
  ' 2. Hidden wallet — enter passphrase on this computer (pinentry)',
96
103
  ' 3. Hidden wallet — enter passphrase on device screen',
104
+ ' 4. Attach PIN wallet — enter Attach PIN on device screen',
97
105
  '',
98
106
  ].join('\n'));
99
107
  rl.question('Enter selection [1/2/3]: ', answer => {
100
108
  const n = answer.trim();
101
- if (n === '1' || n === '2' || n === '3') {
109
+ if (n === '1' || n === '2' || n === '3' || n === '4') {
102
110
  rl.close();
103
111
  resolvePassphraseByChoice(n).then(resolve);
104
112
  return;
105
113
  }
106
- process.stderr.write('Invalid selection. Enter 1, 2, or 3.\n');
114
+ process.stderr.write('Invalid selection. Enter 1, 2, 3, or 4.\n');
107
115
  prompt();
108
116
  });
109
117
  };
@@ -147,6 +155,7 @@ function registerEventHandlers(sdk) {
147
155
  payload: {
148
156
  value: result.value,
149
157
  passphraseOnDevice: result.passphraseOnDevice,
158
+ attachPinOnDevice: result.attachPinOnDevice,
150
159
  save: false,
151
160
  },
152
161
  });
@@ -168,12 +177,12 @@ function registerEventHandlers(sdk) {
168
177
  process.stderr.write('[onekey-hw] Please confirm the action on your device...\n');
169
178
  }
170
179
  });
171
- sdk.on(hd_core_1.DEVICE.CONNECT, (device) => {
180
+ sdk.on(hd_core_1.DEVICE.CONNECT, ({ device }) => {
172
181
  const name = device?.label || device?.name;
173
182
  if (name)
174
183
  process.stderr.write(`[onekey-hw] Device connected: ${name}\n`);
175
184
  });
176
- sdk.on(hd_core_1.DEVICE.DISCONNECT, (device) => {
185
+ sdk.on(hd_core_1.DEVICE.DISCONNECT, ({ device }) => {
177
186
  const name = device?.label || device?.name;
178
187
  if (name)
179
188
  process.stderr.write(`[onekey-hw] Device disconnected: ${name}\n`);
@@ -222,7 +231,7 @@ async function disposeSDK() {
222
231
  return;
223
232
  try {
224
233
  const sdk = await sdkReadyPromise;
225
- sdk.dispose();
234
+ await Promise.resolve(sdk.dispose());
226
235
  }
227
236
  catch {
228
237
  // ignore errors during cleanup
package/dist/session.d.ts CHANGED
@@ -1,21 +1,14 @@
1
1
  /**
2
2
  * Passphrase session management for hd-cli.
3
3
  *
4
- * Aligns with app-monorepo's CLI pattern:
5
- * Login: getPassphraseState passphraseState + sessionId keychain
6
- * Command: keychain → preloadSessionCache → SDK call (no passphrase prompt)
7
- * Stale: error 112 → clear keychain → re-prompt → retry
8
- * Logout: keychain delete
4
+ * Existing keychain entries remain readable for compatibility, but the public
5
+ * SDK no longer exposes new device session ids for persistence.
9
6
  */
10
7
  /**
11
8
  * Load passphraseState + sessionId from keychain and call preloadSessionCache.
12
9
  * Non-fatal: returns the loaded passphraseState or undefined.
13
10
  */
14
11
  export declare function preloadSessionFromKeychain(deviceId: string): Promise<string | undefined>;
15
- /**
16
- * Save passphraseState + sessionId to keychain for next CLI invocation.
17
- */
18
- export declare function saveSessionToKeychain(deviceId: string, passphraseState: string, sessionId: string): Promise<void>;
19
12
  /**
20
13
  * Clear cached session from keychain.
21
14
  */
package/dist/session.js CHANGED
@@ -2,14 +2,11 @@
2
2
  /**
3
3
  * Passphrase session management for hd-cli.
4
4
  *
5
- * Aligns with app-monorepo's CLI pattern:
6
- * Login: getPassphraseState passphraseState + sessionId keychain
7
- * Command: keychain → preloadSessionCache → SDK call (no passphrase prompt)
8
- * Stale: error 112 → clear keychain → re-prompt → retry
9
- * Logout: keychain delete
5
+ * Existing keychain entries remain readable for compatibility, but the public
6
+ * SDK no longer exposes new device session ids for persistence.
10
7
  */
11
8
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.clearSessionFromKeychain = exports.saveSessionToKeychain = exports.preloadSessionFromKeychain = void 0;
9
+ exports.clearSessionFromKeychain = exports.preloadSessionFromKeychain = void 0;
13
10
  const hd_core_1 = require("@onekeyfe/hd-core");
14
11
  const storage_1 = require("./storage");
15
12
  // Keychain key format: scoped by deviceId for multi-device support
@@ -50,22 +47,6 @@ async function preloadSessionFromKeychain(deviceId) {
50
47
  return undefined;
51
48
  }
52
49
  exports.preloadSessionFromKeychain = preloadSessionFromKeychain;
53
- /**
54
- * Save passphraseState + sessionId to keychain for next CLI invocation.
55
- */
56
- async function saveSessionToKeychain(deviceId, passphraseState, sessionId) {
57
- try {
58
- const storage = getStorage();
59
- await Promise.all([
60
- storage.set(psKey(deviceId), Buffer.from(passphraseState, 'utf-8')),
61
- storage.set(sidKey(deviceId), Buffer.from(sessionId, 'utf-8')),
62
- ]);
63
- }
64
- catch {
65
- // Non-fatal — session still works in-memory for this invocation
66
- }
67
- }
68
- exports.saveSessionToKeychain = saveSessionToKeychain;
69
50
  /**
70
51
  * Clear cached session from keychain.
71
52
  */
@@ -3,20 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createNobleBlePlugin = void 0;
4
4
  const hd_shared_1 = require("@onekeyfe/hd-shared");
5
5
  const ONEKEY_SERVICE_UUIDS = [hd_shared_1.ONEKEY_SERVICE_UUID];
6
- const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
7
- const NORMALIZED_WRITE_UUID = '0002';
8
- const NORMALIZED_NOTIFY_UUID = '0003';
9
- const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
10
- ...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
11
- '0001',
12
- ]);
6
+ const ONEKEY_SERVICE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_SERVICE_UUID);
7
+ const ONEKEY_WRITE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_WRITE_CHARACTERISTIC_UUID);
8
+ const ONEKEY_NOTIFY_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
13
9
  const BLUETOOTH_INIT_TIMEOUT = 10000;
14
10
  const DEVICE_SCAN_TIMEOUT = 8000;
15
11
  const CONNECTION_TIMEOUT = 8000;
16
12
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
17
13
  const BLE_CLEANUP_TIMEOUT = 100;
18
14
  const BLE_PACKET_SIZE = 192;
19
- const BLE_WRITE_DELAY = 5;
20
15
  const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
21
16
  let noble = null;
22
17
  let nobleReadyPromise = null;
@@ -25,24 +20,14 @@ const connectedDevices = new Map();
25
20
  const deviceCharacteristics = new Map();
26
21
  const notificationStates = new Map();
27
22
  const notificationGenerations = new Map();
28
- function getBleUuidKey(uuid) {
29
- const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
30
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
31
- }
32
- function isGenericBleService(uuid) {
33
- return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
34
- }
35
- function hasOneKeyAdvertisementService(peripheral) {
36
- const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
37
- return serviceUuids.some(uuid => {
38
- const uuidKey = getBleUuidKey(uuid);
39
- return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
40
- PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
41
- });
42
- }
43
23
  function isOneKeyPeripheral(peripheral) {
44
- const deviceName = peripheral.advertisement?.localName || null;
45
- return (0, hd_shared_1.isOnekeyDevice)(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
24
+ const serviceUuids = peripheral.advertisement?.serviceUuids;
25
+ return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
26
+ (0, hd_shared_1.isOnekeyBluetoothDevice)({
27
+ id: peripheral.id,
28
+ localName: peripheral.advertisement?.localName,
29
+ serviceUuids,
30
+ }));
46
31
  }
47
32
  function enqueueNotification(deviceId, generation, data) {
48
33
  const state = notificationStates.get(deviceId);
@@ -172,7 +157,7 @@ async function scanDevices(targetDeviceId) {
172
157
  const onDiscover = (peripheral) => {
173
158
  if (targetDeviceId && peripheral.id !== targetDeviceId)
174
159
  return;
175
- if (!targetDeviceId && !isOneKeyPeripheral(peripheral))
160
+ if (!isOneKeyPeripheral(peripheral))
176
161
  return;
177
162
  discoveredDevices.set(peripheral.id, peripheral);
178
163
  found.set(peripheral.id, peripheral);
@@ -221,13 +206,7 @@ async function discoverCharacteristics(peripheral) {
221
206
  resolve(discoveredServices);
222
207
  });
223
208
  });
224
- let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid)));
225
- if (!service) {
226
- service =
227
- services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
228
- services.find(s => !isGenericBleService(s.uuid)) ||
229
- services[0];
230
- }
209
+ const service = services.find(s => (0, hd_shared_1.matchesKnownBleUuid)(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
231
210
  if (!service) {
232
211
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
233
212
  }
@@ -244,11 +223,10 @@ async function discoverCharacteristics(peripheral) {
244
223
  let writeCharacteristic;
245
224
  let notifyCharacteristic;
246
225
  for (const characteristic of characteristics) {
247
- const uuidKey = getBleUuidKey(characteristic.uuid);
248
- if (uuidKey === NORMALIZED_WRITE_UUID) {
226
+ if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
249
227
  writeCharacteristic = characteristic;
250
228
  }
251
- else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
229
+ else if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
252
230
  notifyCharacteristic = characteristic;
253
231
  }
254
232
  }
@@ -288,9 +266,9 @@ function subscribeNotifications(deviceId, generation, notifyCharacteristic) {
288
266
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotifyChangeFailure);
289
267
  });
290
268
  }
291
- function writeCharacteristic(characteristic, buffer) {
269
+ function writeCharacteristic(characteristic, buffer, withoutResponse) {
292
270
  return new Promise((resolve, reject) => {
293
- characteristic.write(buffer, true, (error) => {
271
+ characteristic.write(buffer, withoutResponse, (error) => {
294
272
  if (error) {
295
273
  reject(error);
296
274
  return;
@@ -336,33 +314,39 @@ function createNobleBlePlugin() {
336
314
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
337
315
  }
338
316
  await connectPeripheral(peripheral);
339
- const characteristics = await discoverCharacteristics(peripheral);
340
- const notificationState = createNotificationState(uuid);
317
+ let characteristics;
341
318
  try {
319
+ characteristics = await discoverCharacteristics(peripheral);
320
+ const notificationState = createNotificationState(uuid);
342
321
  await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
322
+ connectedDevices.set(uuid, peripheral);
323
+ deviceCharacteristics.set(uuid, characteristics);
343
324
  }
344
325
  catch (error) {
345
326
  clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
327
+ if (characteristics) {
328
+ characteristics.notify.removeAllListeners('data');
329
+ await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
330
+ }
331
+ if (peripheral.state !== 'disconnected') {
332
+ await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
333
+ }
346
334
  throw error;
347
335
  }
348
- connectedDevices.set(uuid, peripheral);
349
- deviceCharacteristics.set(uuid, characteristics);
350
336
  },
351
337
  async disconnect(uuid) {
352
338
  await disconnectDevice(uuid);
353
339
  },
354
- async send(uuid, data) {
340
+ async send(uuid, data, options) {
355
341
  const characteristics = deviceCharacteristics.get(uuid);
356
342
  if (!characteristics) {
357
343
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotFound, `BLE device is not connected: ${uuid}`);
358
344
  }
359
345
  const buffer = Buffer.from(data, 'hex');
346
+ const withoutResponse = options?.withoutResponse ?? true;
360
347
  for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
361
348
  const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
362
- await writeCharacteristic(characteristics.write, chunk);
363
- if (offset + BLE_PACKET_SIZE < buffer.length) {
364
- await (0, hd_shared_1.wait)(BLE_WRITE_DELAY);
365
- }
349
+ await writeCharacteristic(characteristics.write, chunk, withoutResponse);
366
350
  }
367
351
  },
368
352
  async receive(uuid) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hardware-cli",
3
- "version": "1.2.0-alpha.9",
3
+ "version": "1.2.0-alpha.90",
4
4
  "description": "OneKey hardware wallet CLI for testing device communication",
5
5
  "author": "OneKey",
6
6
  "license": "Apache-2.0",
@@ -23,6 +23,7 @@
23
23
  "start": "node dist/cli.js",
24
24
  "search": "node dist/cli.js search",
25
25
  "get-features": "node dist/cli.js get-features",
26
+ "get-state": "node dist/cli.js get-state",
26
27
  "get-address": "node dist/cli.js get-address",
27
28
  "ping": "node dist/cli.js ping",
28
29
  "lint": "eslint .",
@@ -30,12 +31,12 @@
30
31
  "test": "jest"
31
32
  },
32
33
  "dependencies": {
33
- "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.9",
34
- "@onekeyfe/hd-core": "1.2.0-alpha.9",
35
- "@onekeyfe/hd-shared": "1.2.0-alpha.9",
36
- "@onekeyfe/hd-transport-usb": "1.2.0-alpha.9",
34
+ "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.90",
35
+ "@onekeyfe/hd-core": "1.2.0-alpha.90",
36
+ "@onekeyfe/hd-shared": "1.2.0-alpha.90",
37
+ "@onekeyfe/hd-transport-usb": "1.2.0-alpha.90",
37
38
  "@stoprocent/noble": "2.3.16",
38
39
  "commander": "^12.0.0"
39
40
  },
40
- "gitHead": "f90ef9921b742f68cce9f4e7a0425e45e8ae7ec9"
41
+ "gitHead": "e8271225634f3058498af8beee06d9de705b6710"
41
42
  }
@@ -0,0 +1,8 @@
1
+ import packageJson from '../../package.json';
2
+ import { program } from '../cli';
3
+
4
+ describe('CLI 版本', () => {
5
+ test('与发布包 package.json 使用同一个版本来源', () => {
6
+ expect(program.version()).toBe(packageJson.version);
7
+ });
8
+ });
@@ -0,0 +1,29 @@
1
+ import { selectSearchDevice } from '../deviceSelection';
2
+
3
+ describe('selectSearchDevice', () => {
4
+ it('多设备环境优先选择显式 connectId 对应的设备', () => {
5
+ const devices = [
6
+ { connectId: 'first-device', name: 'Pro A9CA' },
7
+ { connectId: 'target-device', name: 'Pro2 6136' },
8
+ ];
9
+
10
+ expect(selectSearchDevice(devices, 'target-device')).toEqual(devices[1]);
11
+ });
12
+
13
+ it('未指定 connectId 时保持选择第一台设备的兼容行为', () => {
14
+ const devices = [
15
+ { connectId: 'first-device', name: 'Pro2 6136' },
16
+ { connectId: 'second-device', name: 'Pro2 C445' },
17
+ ];
18
+
19
+ expect(selectSearchDevice(devices)).toEqual(devices[0]);
20
+ });
21
+
22
+ it('显式 connectId 暂未出现在扫描结果时仍保留该目标', () => {
23
+ const devices = [{ connectId: 'other-device', name: 'Pro2 C445' }];
24
+
25
+ expect(selectSearchDevice(devices, 'target-device')).toEqual({
26
+ connectId: 'target-device',
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,118 @@
1
+ import { getCanonicalDeviceState, getCompatibleFeatures } from '../deviceStateCommands';
2
+
3
+ const createSdkMock = () => ({
4
+ searchDevices: jest.fn(),
5
+ getDeviceState: jest.fn(),
6
+ getFeatures: jest.fn(),
7
+ });
8
+
9
+ describe('设备状态 CLI 兼容层', () => {
10
+ test('Protocol V2 直接返回 SDK 搜索结果中的兼容 features', async () => {
11
+ const sdk = createSdkMock();
12
+ const features = {
13
+ protocol: 'V2',
14
+ deviceType: 'pro2',
15
+ deviceId: 'device-id',
16
+ };
17
+ sdk.searchDevices.mockResolvedValue({
18
+ success: true,
19
+ payload: [
20
+ {
21
+ connectId: 'pro2-connect-id',
22
+ state: { protocol: 'V2' },
23
+ features,
24
+ },
25
+ ],
26
+ });
27
+
28
+ await expect(getCompatibleFeatures(sdk as never, 'pro2-connect-id')).resolves.toEqual({
29
+ success: true,
30
+ payload: features,
31
+ });
32
+ expect(sdk.getFeatures).not.toHaveBeenCalled();
33
+ });
34
+
35
+ test('Protocol V1 继续调用公共 getFeatures 保持旧行为', async () => {
36
+ const sdk = createSdkMock();
37
+ const response = {
38
+ success: true,
39
+ payload: { protocol: 'V1', label: 'Classic' },
40
+ };
41
+ sdk.searchDevices.mockResolvedValue({
42
+ success: true,
43
+ payload: [
44
+ {
45
+ connectId: 'classic-connect-id',
46
+ state: { protocol: 'V1' },
47
+ },
48
+ ],
49
+ });
50
+ sdk.getFeatures.mockResolvedValue(response);
51
+
52
+ await expect(getCompatibleFeatures(sdk as never, 'classic-connect-id')).resolves.toBe(response);
53
+ expect(sdk.getFeatures).toHaveBeenCalledWith('classic-connect-id');
54
+ });
55
+
56
+ test('未显式指定设备时选择搜索结果中的第一台设备', async () => {
57
+ const sdk = createSdkMock();
58
+ const response = { success: true, payload: { protocol: 'V1' } };
59
+ sdk.searchDevices.mockResolvedValue({
60
+ success: true,
61
+ payload: [{ connectId: 'first-device', state: { protocol: 'V1' } }],
62
+ });
63
+ sdk.getFeatures.mockResolvedValue(response);
64
+
65
+ await expect(getCompatibleFeatures(sdk as never)).resolves.toBe(response);
66
+ expect(sdk.getFeatures).toHaveBeenCalledWith('first-device');
67
+ });
68
+
69
+ test('显式 connectId 不在搜索结果时返回结构化错误', async () => {
70
+ const sdk = createSdkMock();
71
+ sdk.searchDevices.mockResolvedValue({
72
+ success: true,
73
+ payload: [{ connectId: 'another-device', state: { protocol: 'V1' } }],
74
+ });
75
+
76
+ await expect(getCompatibleFeatures(sdk as never, 'missing-device')).resolves.toEqual({
77
+ success: false,
78
+ payload: {
79
+ code: 'DEVICE_NOT_FOUND',
80
+ error: 'Device not found: missing-device',
81
+ },
82
+ });
83
+ expect(sdk.getFeatures).not.toHaveBeenCalled();
84
+ });
85
+
86
+ test('get-state 将 firmware scope 传给 SDK', async () => {
87
+ const sdk = createSdkMock();
88
+ const response = { success: true, payload: { protocol: 'V2' } };
89
+ sdk.searchDevices.mockResolvedValue({
90
+ success: true,
91
+ payload: [{ connectId: 'pro2-connect-id', state: { protocol: 'V2' } }],
92
+ });
93
+ sdk.getDeviceState.mockResolvedValue(response);
94
+
95
+ await expect(
96
+ getCanonicalDeviceState(sdk as never, 'pro2-connect-id', 'firmware')
97
+ ).resolves.toBe(response);
98
+ expect(sdk.getDeviceState).toHaveBeenCalledWith('pro2-connect-id', {
99
+ scope: 'firmware',
100
+ });
101
+ expect(sdk.searchDevices).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ test('get-state 未指定 connectId 时搜索并选择第一台设备', async () => {
105
+ const sdk = createSdkMock();
106
+ const state = { protocol: 'V2', revision: 2 };
107
+ sdk.searchDevices.mockResolvedValue({
108
+ success: true,
109
+ payload: [{ connectId: 'pro2-connect-id', state }],
110
+ });
111
+
112
+ await expect(getCanonicalDeviceState(sdk as never, undefined, 'runtime')).resolves.toEqual({
113
+ success: true,
114
+ payload: state,
115
+ });
116
+ expect(sdk.getDeviceState).not.toHaveBeenCalled();
117
+ });
118
+ });
@@ -0,0 +1,18 @@
1
+ import { getLegacyFirmwareConnectTimeout, program } from '../cli';
2
+
3
+ describe('firmware-update-legacy CLI command', () => {
4
+ test('提供 Classic/Pure 的本地固件升级命令', () => {
5
+ const command = program.commands.find(item => item.name() === 'firmware-update-legacy');
6
+
7
+ expect(command).toBeDefined();
8
+ expect(command?.description()).toBe('Update Classic/Pure firmware through the legacy protocol');
9
+ expect(command?.options.find(option => option.long === '--binary')?.mandatory).toBe(true);
10
+ expect(command?.options.some(option => option.long === '--device-name')).toBe(true);
11
+ expect(command?.options.some(option => option.long === '--update-type')).toBe(true);
12
+ });
13
+
14
+ test('USB Classic 固件升级使用足够的设备探测超时', () => {
15
+ expect(getLegacyFirmwareConnectTimeout('usb')).toBe(90_000);
16
+ expect(getLegacyFirmwareConnectTimeout('ble')).toBeUndefined();
17
+ });
18
+ });