@onekeyfe/hardware-cli 1.2.0-alpha.1 → 1.2.0-alpha.100
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/dist/cli.d.ts +83 -1
- package/dist/cli.js +501 -142
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +12 -2
- package/dist/sdk.js +22 -11
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +371 -0
- package/package.json +8 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +101 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +687 -173
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +29 -13
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +487 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function selectSearchDevice<T extends { connectId?: string | null }>(
|
|
2
|
+
devices: T[],
|
|
3
|
+
preferredConnectId?: string
|
|
4
|
+
): T | undefined {
|
|
5
|
+
if (preferredConnectId) {
|
|
6
|
+
return (
|
|
7
|
+
devices.find(device => device.connectId === preferredConnectId) ??
|
|
8
|
+
({ connectId: preferredConnectId } as T)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return devices[0];
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CoreApi, DeviceStateScope, KnownDevice, SearchDevice } from '@onekeyfe/hd-core';
|
|
2
|
+
|
|
3
|
+
type DeviceStateSdk = Pick<CoreApi, 'searchDevices' | 'getDeviceState' | 'getFeatures'>;
|
|
4
|
+
|
|
5
|
+
type DiscoveredDevice = SearchDevice & Partial<Pick<KnownDevice, 'features' | 'state'>>;
|
|
6
|
+
|
|
7
|
+
const createDeviceNotFoundResult = (connectId?: string) => ({
|
|
8
|
+
success: false as const,
|
|
9
|
+
payload: {
|
|
10
|
+
code: 'DEVICE_NOT_FOUND',
|
|
11
|
+
error: connectId ? `Device not found: ${connectId}` : 'No device found',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const resolveSearchDevice = async (sdk: DeviceStateSdk, connectId?: string) => {
|
|
16
|
+
const searchResult = await sdk.searchDevices();
|
|
17
|
+
if (!searchResult.success) return searchResult;
|
|
18
|
+
|
|
19
|
+
const devices = searchResult.payload as DiscoveredDevice[];
|
|
20
|
+
const device = connectId ? devices.find(item => item.connectId === connectId) : devices[0];
|
|
21
|
+
|
|
22
|
+
if (!device?.connectId) return createDeviceNotFoundResult(connectId);
|
|
23
|
+
return { success: true as const, payload: device };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unified state entry for the new CLI. Resolve the user-facing connectId through
|
|
28
|
+
* discovery so V1 serial IDs map to the process-local USB path.
|
|
29
|
+
*/
|
|
30
|
+
export const getCanonicalDeviceState = async (
|
|
31
|
+
sdk: DeviceStateSdk,
|
|
32
|
+
connectId: string | undefined,
|
|
33
|
+
scope: DeviceStateScope
|
|
34
|
+
) => {
|
|
35
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
36
|
+
if (!deviceResult.success) return deviceResult;
|
|
37
|
+
|
|
38
|
+
if (scope === 'runtime' && deviceResult.payload.state) {
|
|
39
|
+
return { success: true as const, payload: deviceResult.payload.state };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resolvedConnectId = deviceResult.payload.connectId ?? undefined;
|
|
43
|
+
if (!resolvedConnectId) return createDeviceNotFoundResult();
|
|
44
|
+
return sdk.getDeviceState(resolvedConnectId, { scope });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
|
|
49
|
+
* Public SDK.getFeatures remains V1-only.
|
|
50
|
+
*/
|
|
51
|
+
export const getCompatibleFeatures = async (sdk: DeviceStateSdk, connectId?: string) => {
|
|
52
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
53
|
+
if (!deviceResult.success) return deviceResult;
|
|
54
|
+
|
|
55
|
+
const device = deviceResult.payload;
|
|
56
|
+
const protocol = device.state?.protocol ?? device.features?.protocol;
|
|
57
|
+
if (protocol === 'V2') {
|
|
58
|
+
if (!device.features) {
|
|
59
|
+
return {
|
|
60
|
+
success: false as const,
|
|
61
|
+
payload: {
|
|
62
|
+
code: 'DEVICE_FEATURES_UNAVAILABLE',
|
|
63
|
+
error: 'Protocol V2 compatibility features are unavailable',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { success: true as const, payload: device.features };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return sdk.getFeatures(device.connectId ?? '');
|
|
71
|
+
};
|
package/src/pinentry.ts
CHANGED
package/src/sdk.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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
|
-
* -
|
|
9
|
-
*
|
|
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
|
|
|
12
12
|
import * as readline from 'node:readline';
|
|
@@ -14,14 +14,17 @@ import HardwareSDK from '@onekeyfe/hd-common-connect-sdk';
|
|
|
14
14
|
import { DEVICE, UI_EVENT, UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core';
|
|
15
15
|
|
|
16
16
|
import { promptPassphraseViaPinentry } from './pinentry';
|
|
17
|
+
import { createNobleBlePlugin } from './transports/nobleBlePlugin';
|
|
17
18
|
|
|
18
|
-
import type { ConnectSettings } from '@onekeyfe/hd-core';
|
|
19
|
+
import type { ConnectSettings, KnownDevice } from '@onekeyfe/hd-core';
|
|
19
20
|
import type { PinentryResult } from './pinentry';
|
|
20
21
|
|
|
21
22
|
export interface SDKOptions {
|
|
22
23
|
connectId?: string;
|
|
23
24
|
passphraseState?: string;
|
|
24
25
|
useEmptyPassphrase?: boolean;
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
transport?: 'usb' | 'ble';
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -56,9 +59,15 @@ let sdkReadyPromise: Promise<typeof HardwareSDK> | null = null;
|
|
|
56
59
|
* 2. Hidden wallet — enter passphrase via pinentry (secure OS dialog)
|
|
57
60
|
* 3. Hidden wallet — enter passphrase on device screen
|
|
58
61
|
*/
|
|
59
|
-
function resolvePassphraseByChoice(choice: '1' | '2' | '3'): Promise<PinentryResult> {
|
|
62
|
+
export function resolvePassphraseByChoice(choice: '1' | '2' | '3' | '4'): Promise<PinentryResult> {
|
|
60
63
|
if (choice === '1') return Promise.resolve({ value: '', passphraseOnDevice: false });
|
|
61
64
|
if (choice === '2') return promptPassphraseViaPinentry();
|
|
65
|
+
if (choice === '4')
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
value: '',
|
|
68
|
+
passphraseOnDevice: false,
|
|
69
|
+
attachPinOnDevice: true,
|
|
70
|
+
});
|
|
62
71
|
return Promise.resolve({ value: '', passphraseOnDevice: true });
|
|
63
72
|
}
|
|
64
73
|
|
|
@@ -81,18 +90,19 @@ function promptPassphraseMode(): Promise<PinentryResult> {
|
|
|
81
90
|
' 1. Standard wallet (no passphrase)',
|
|
82
91
|
' 2. Hidden wallet — enter passphrase on this computer (pinentry)',
|
|
83
92
|
' 3. Hidden wallet — enter passphrase on device screen',
|
|
93
|
+
' 4. Attach PIN wallet — enter Attach PIN on device screen',
|
|
84
94
|
'',
|
|
85
95
|
].join('\n')
|
|
86
96
|
);
|
|
87
97
|
|
|
88
98
|
rl.question('Enter selection [1/2/3]: ', answer => {
|
|
89
|
-
const n = answer.trim() as '1' | '2' | '3';
|
|
90
|
-
if (n === '1' || n === '2' || n === '3') {
|
|
99
|
+
const n = answer.trim() as '1' | '2' | '3' | '4';
|
|
100
|
+
if (n === '1' || n === '2' || n === '3' || n === '4') {
|
|
91
101
|
rl.close();
|
|
92
102
|
resolvePassphraseByChoice(n).then(resolve);
|
|
93
103
|
return;
|
|
94
104
|
}
|
|
95
|
-
process.stderr.write('Invalid selection. Enter 1, 2, or
|
|
105
|
+
process.stderr.write('Invalid selection. Enter 1, 2, 3, or 4.\n');
|
|
96
106
|
prompt();
|
|
97
107
|
});
|
|
98
108
|
};
|
|
@@ -140,6 +150,7 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
140
150
|
payload: {
|
|
141
151
|
value: result.value,
|
|
142
152
|
passphraseOnDevice: result.passphraseOnDevice,
|
|
153
|
+
attachPinOnDevice: result.attachPinOnDevice,
|
|
143
154
|
save: false,
|
|
144
155
|
},
|
|
145
156
|
});
|
|
@@ -166,12 +177,12 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
166
177
|
}
|
|
167
178
|
});
|
|
168
179
|
|
|
169
|
-
sdk.on(DEVICE.CONNECT, (device:
|
|
180
|
+
sdk.on(DEVICE.CONNECT, ({ device }: { device: KnownDevice }) => {
|
|
170
181
|
const name = device?.label || device?.name;
|
|
171
182
|
if (name) process.stderr.write(`[onekey-hw] Device connected: ${name}\n`);
|
|
172
183
|
});
|
|
173
184
|
|
|
174
|
-
sdk.on(DEVICE.DISCONNECT, (device:
|
|
185
|
+
sdk.on(DEVICE.DISCONNECT, ({ device }: { device: KnownDevice }) => {
|
|
175
186
|
const name = device?.label || device?.name;
|
|
176
187
|
if (name) process.stderr.write(`[onekey-hw] Device disconnected: ${name}\n`);
|
|
177
188
|
});
|
|
@@ -182,12 +193,17 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
182
193
|
// ---------------------------------------------------------------------------
|
|
183
194
|
|
|
184
195
|
async function initSDK(): Promise<typeof HardwareSDK> {
|
|
196
|
+
const transport = currentOpts.transport ?? 'usb';
|
|
185
197
|
const settings: Partial<ConnectSettings> = {
|
|
186
|
-
debug: false,
|
|
198
|
+
debug: currentOpts.debug ?? false,
|
|
187
199
|
fetchConfig: true,
|
|
188
|
-
env: 'node-usb',
|
|
200
|
+
env: transport === 'ble' ? 'lowlevel' : 'node-usb',
|
|
189
201
|
};
|
|
190
|
-
await HardwareSDK.init(
|
|
202
|
+
await HardwareSDK.init(
|
|
203
|
+
settings,
|
|
204
|
+
undefined,
|
|
205
|
+
transport === 'ble' ? createNobleBlePlugin() : undefined
|
|
206
|
+
);
|
|
191
207
|
|
|
192
208
|
// Defensive: strip any stale listeners (e.g. left over from a previous
|
|
193
209
|
// dispose/init cycle in a long-running process) before wiring ours.
|
|
@@ -223,7 +239,7 @@ export async function disposeSDK(): Promise<void> {
|
|
|
223
239
|
if (!sdkReadyPromise) return;
|
|
224
240
|
try {
|
|
225
241
|
const sdk = await sdkReadyPromise;
|
|
226
|
-
sdk.dispose();
|
|
242
|
+
await Promise.resolve(sdk.dispose());
|
|
227
243
|
} catch {
|
|
228
244
|
// ignore errors during cleanup
|
|
229
245
|
} finally {
|
package/src/session.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Passphrase session management for hd-cli.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
import { preloadSessionCache } from '@onekeyfe/hd-core';
|
|
@@ -55,25 +52,6 @@ export async function preloadSessionFromKeychain(deviceId: string): Promise<stri
|
|
|
55
52
|
return undefined;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
/**
|
|
59
|
-
* Save passphraseState + sessionId to keychain for next CLI invocation.
|
|
60
|
-
*/
|
|
61
|
-
export async function saveSessionToKeychain(
|
|
62
|
-
deviceId: string,
|
|
63
|
-
passphraseState: string,
|
|
64
|
-
sessionId: string
|
|
65
|
-
): Promise<void> {
|
|
66
|
-
try {
|
|
67
|
-
const storage = getStorage();
|
|
68
|
-
await Promise.all([
|
|
69
|
-
storage.set(psKey(deviceId), Buffer.from(passphraseState, 'utf-8')),
|
|
70
|
-
storage.set(sidKey(deviceId), Buffer.from(sessionId, 'utf-8')),
|
|
71
|
-
]);
|
|
72
|
-
} catch {
|
|
73
|
-
// Non-fatal — session still works in-memory for this invocation
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
55
|
/**
|
|
78
56
|
* Clear cached session from keychain.
|
|
79
57
|
*/
|