@onekeyfe/hardware-cli 1.1.26-alpha.106 → 1.1.26-alpha.4

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 (41) hide show
  1. package/.eslintignore +4 -0
  2. package/dist/chains.d.ts +6 -0
  3. package/dist/chains.js +191 -87
  4. package/dist/cli.js +615 -496
  5. package/dist/index.d.ts +16 -89
  6. package/dist/index.js +1 -2
  7. package/dist/sdk.d.ts +15 -5
  8. package/dist/sdk.js +237 -131
  9. package/dist/session.d.ts +22 -0
  10. package/dist/session.js +83 -0
  11. package/dist/storage/index.d.ts +2 -0
  12. package/dist/storage/index.js +5 -0
  13. package/dist/storage/process-utils.d.ts +2 -0
  14. package/dist/storage/process-utils.js +44 -0
  15. package/dist/storage/secure-storage.linux.d.ts +11 -0
  16. package/dist/storage/secure-storage.linux.js +59 -0
  17. package/dist/storage/secure-storage.macos.d.ts +11 -0
  18. package/dist/storage/secure-storage.macos.js +65 -0
  19. package/dist/storage/storage-factory.d.ts +3 -0
  20. package/dist/storage/storage-factory.js +14 -0
  21. package/dist/storage/types.d.ts +18 -0
  22. package/dist/storage/types.js +2 -0
  23. package/package.json +15 -13
  24. package/src/chains.ts +229 -85
  25. package/src/cli.ts +620 -297
  26. package/src/sdk.ts +244 -125
  27. package/src/session.ts +89 -0
  28. package/src/storage/index.ts +2 -0
  29. package/src/storage/process-utils.ts +50 -0
  30. package/src/storage/secure-storage.linux.ts +68 -0
  31. package/src/storage/secure-storage.macos.ts +68 -0
  32. package/src/storage/storage-factory.ts +13 -0
  33. package/src/storage/types.ts +17 -0
  34. package/tsconfig.json +5 -7
  35. package/.claude-plugin/plugin.json +0 -14
  36. package/AGENTS.md +0 -40
  37. package/CLAUDE.md +0 -40
  38. package/README.md +0 -112
  39. package/evals/cases.json +0 -373
  40. package/evals/run-evals.sh +0 -136
  41. package/rollup.config.js +0 -28
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
-
2
+ "use strict";
4
3
  Object.defineProperty(exports, "__esModule", { value: true });
5
4
  const commander_1 = require("commander");
6
5
  const sdk_1 = require("./sdk");
6
+ const session_1 = require("./session");
7
7
  const chains_1 = require("./chains");
8
8
  const program = new commander_1.Command();
9
9
  program
10
10
  .name('onekey-hw')
11
11
  .description('OneKey hardware wallet CLI for AI agent integration')
12
- .version('1.1.25-alpha.1');
12
+ .version('1.1.26-alpha.1');
13
13
  // ============================================================
14
14
  // Global Options
15
15
  // ============================================================
@@ -23,36 +23,53 @@ program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase p
23
23
  program
24
24
  .command('search')
25
25
  .description('Search for connected OneKey hardware wallet devices')
26
- .action(async () => {
27
- const globalOpts = program.opts();
28
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
29
- try {
30
- const result = await sdk.searchDevices();
31
- // Auto-fetch features for each discovered device (doesn't require PIN)
32
- if (result?.success && Array.isArray(result.payload)) {
33
- for (const device of result.payload) {
34
- if (device.connectId) {
35
- try {
36
- const features = await sdk.getFeatures(device.connectId);
37
- if (features?.success && features.payload) {
38
- device.features = features.payload;
39
- device.name = features.payload.label || features.payload.ble_name || device.name;
40
- device.deviceType =
41
- features.payload.onekey_device_type?.toLowerCase() || device.deviceType;
26
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
27
+ const result = await sdk.searchDevices();
28
+ // Auto-fetch features for each discovered device (doesn't require PIN)
29
+ if (result?.success && Array.isArray(result.payload)) {
30
+ for (const device of result.payload) {
31
+ if (device.connectId) {
32
+ try {
33
+ const features = await sdk.getFeatures(device.connectId);
34
+ if (features?.success && features.payload) {
35
+ device.features = features.payload;
36
+ device.name = features.payload.label || features.payload.ble_name || device.name;
37
+ const devType = features.payload.onekey_device_type?.toLowerCase();
38
+ if (devType) {
39
+ device.deviceType = devType;
42
40
  }
43
41
  }
44
- catch {
45
- // Features fetch failed — device may need PIN, continue with basic info
46
- }
42
+ }
43
+ catch {
44
+ // Features fetch failed — device may need PIN, continue with basic info
47
45
  }
48
46
  }
49
47
  }
50
- outputResult(globalOpts, result);
51
48
  }
52
- finally {
53
- sdk.dispose();
49
+ outputResult(globalOpts, result);
50
+ }));
51
+ program
52
+ .command('get-features')
53
+ .description('Get device features (firmware, unlock state, passphrase protection, etc.)')
54
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
55
+ // Resolve connectId: explicit flag wins, else pick the first attached device
56
+ let { connectId } = globalOpts;
57
+ if (!connectId) {
58
+ const searchResult = await sdk.searchDevices();
59
+ if (!searchResult?.success ||
60
+ !Array.isArray(searchResult.payload) ||
61
+ searchResult.payload.length === 0) {
62
+ outputResult(globalOpts, {
63
+ success: false,
64
+ payload: { error: 'No device found', code: 'NO_DEVICE' },
65
+ });
66
+ return;
67
+ }
68
+ connectId = searchResult.payload[0].connectId ?? undefined;
54
69
  }
55
- });
70
+ const result = await sdk.getFeatures(connectId || '');
71
+ outputResult(globalOpts, result);
72
+ }));
56
73
  // ============================================================
57
74
  // Signing Commands
58
75
  // ============================================================
@@ -62,133 +79,89 @@ program
62
79
  .requiredOption('--chain <chain>', 'Target blockchain (evm, btc, sol, ...)')
63
80
  .option('--path <path>', 'BIP44 derivation path')
64
81
  .option('--show-on-device <bool>', 'Display address on device for verification', 'true')
65
- .action(async (opts) => {
66
- const globalOpts = program.opts();
67
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
68
- try {
69
- const result = await (0, chains_1.resolveGetAddress)(sdk, {
70
- chain: opts.chain,
71
- path: opts.path,
72
- showOnDevice: opts.showOnDevice === 'true',
73
- ...getCommonParams(globalOpts),
74
- });
75
- outputResult(globalOpts, result);
76
- }
77
- finally {
78
- sdk.dispose();
79
- }
80
- });
82
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
83
+ const result = await (0, chains_1.resolveGetAddress)(sdk, {
84
+ chain: opts.chain,
85
+ path: opts.path,
86
+ showOnDevice: opts.showOnDevice === 'true',
87
+ ...params,
88
+ });
89
+ outputResult(globalOpts, result);
90
+ }));
81
91
  program
82
92
  .command('get-public-key')
83
93
  .description('Get public key from the hardware wallet')
84
94
  .requiredOption('--chain <chain>', 'Target blockchain')
85
95
  .option('--path <path>', 'BIP44 derivation path')
86
- .action(async (opts) => {
87
- const globalOpts = program.opts();
88
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
89
- try {
90
- const result = await (0, chains_1.resolveGetPublicKey)(sdk, {
91
- chain: opts.chain,
92
- path: opts.path,
93
- ...getCommonParams(globalOpts),
94
- });
95
- outputResult(globalOpts, result);
96
- }
97
- finally {
98
- sdk.dispose();
99
- }
100
- });
96
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
97
+ const result = await (0, chains_1.resolveGetPublicKey)(sdk, {
98
+ chain: opts.chain,
99
+ path: opts.path,
100
+ ...params,
101
+ });
102
+ outputResult(globalOpts, result);
103
+ }));
101
104
  program
102
105
  .command('sign-transaction')
103
106
  .description('Sign a blockchain transaction (requires device confirmation)')
104
107
  .requiredOption('--chain <chain>', 'Target blockchain')
105
108
  .requiredOption('--tx <json>', 'Transaction data (JSON)')
106
109
  .option('--path <path>', 'BIP44 derivation path')
107
- .action(async (opts) => {
108
- const globalOpts = program.opts();
109
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
110
- try {
111
- const tx = safeJsonParse(opts.tx, '--tx');
112
- const result = await (0, chains_1.resolveSignTransaction)(sdk, {
113
- chain: opts.chain,
114
- path: opts.path,
115
- transaction: tx,
116
- ...getCommonParams(globalOpts),
117
- });
118
- outputResult(globalOpts, result);
119
- }
120
- finally {
121
- sdk.dispose();
122
- }
123
- });
110
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
111
+ const tx = safeJsonParse(opts.tx, '--tx');
112
+ const result = await (0, chains_1.resolveSignTransaction)(sdk, {
113
+ chain: opts.chain,
114
+ path: opts.path,
115
+ transaction: tx,
116
+ ...params,
117
+ });
118
+ outputResult(globalOpts, result);
119
+ }));
124
120
  program
125
121
  .command('sign-message')
126
122
  .description('Sign a message (requires device confirmation)')
127
123
  .requiredOption('--chain <chain>', 'Target blockchain')
128
124
  .requiredOption('--message <msg>', 'Message to sign')
129
125
  .option('--path <path>', 'BIP44 derivation path')
130
- .action(async (opts) => {
131
- const globalOpts = program.opts();
132
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
133
- try {
134
- const result = await (0, chains_1.resolveSignMessage)(sdk, {
135
- chain: opts.chain,
136
- path: opts.path,
137
- message: opts.message,
138
- ...getCommonParams(globalOpts),
139
- });
140
- outputResult(globalOpts, result);
141
- }
142
- finally {
143
- sdk.dispose();
144
- }
145
- });
126
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
127
+ const result = await (0, chains_1.resolveSignMessage)(sdk, {
128
+ chain: opts.chain,
129
+ path: opts.path,
130
+ message: opts.message,
131
+ ...params,
132
+ });
133
+ outputResult(globalOpts, result);
134
+ }));
146
135
  program
147
136
  .command('sign-typed-data')
148
137
  .description('Sign EIP-712 typed data (EVM only, requires device confirmation)')
149
138
  .requiredOption('--data <json>', 'EIP-712 typed data JSON')
150
139
  .option('--path <path>', 'BIP44 derivation path')
151
- .option('--metamask-v4-compat', 'Use MetaMask V4 compatibility mode', true)
152
- .action(async (opts) => {
153
- const globalOpts = program.opts();
154
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
155
- try {
156
- const data = safeJsonParse(opts.data, '--data');
157
- const params = getCommonParams(globalOpts);
158
- const path = opts.path || "m/44'/60'/0'/0/0";
159
- const result = await sdk.evmSignTypedData(params.connectId || '', params.deviceId || '', {
160
- path,
161
- metamaskV4Compat: opts.metamaskV4Compat,
162
- data,
163
- ...params,
164
- });
165
- outputResult(globalOpts, result);
166
- }
167
- finally {
168
- sdk.dispose();
169
- }
170
- });
140
+ .option('--no-metamask-v4-compat', 'Disable MetaMask V4 compatibility mode')
141
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
142
+ const data = safeJsonParse(opts.data, '--data');
143
+ const path = opts.path || "m/44'/60'/0'/0/0";
144
+ const result = await sdk.evmSignTypedData(params.connectId || '', params.deviceId || '', {
145
+ path,
146
+ metamaskV4Compat: opts.metamaskV4Compat,
147
+ data,
148
+ ...params,
149
+ });
150
+ outputResult(globalOpts, result);
151
+ }));
171
152
  program
172
153
  .command('sign-psbt')
173
154
  .description('Sign a Bitcoin PSBT (Pro/Classic1s only, requires device confirmation)')
174
155
  .requiredOption('--psbt <hex>', 'Hex-encoded PSBT data')
175
156
  .option('--coin <coin>', 'Bitcoin network: btc, ltc, etc.', 'btc')
176
- .action(async (opts) => {
177
- const globalOpts = program.opts();
178
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
179
- try {
180
- const params = getCommonParams(globalOpts);
181
- const result = await sdk.btcSignPsbt(params.connectId || '', params.deviceId || '', {
182
- psbt: opts.psbt,
183
- coin: opts.coin,
184
- ...params,
185
- });
186
- outputResult(globalOpts, result);
187
- }
188
- finally {
189
- sdk.dispose();
190
- }
191
- });
157
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
158
+ const result = await sdk.btcSignPsbt(params.connectId || '', params.deviceId || '', {
159
+ psbt: opts.psbt,
160
+ coin: opts.coin,
161
+ ...params,
162
+ });
163
+ outputResult(globalOpts, result);
164
+ }));
192
165
  program
193
166
  .command('verify-message')
194
167
  .description('Verify a signed message on-device (BTC, EVM, Starcoin)')
@@ -196,69 +169,57 @@ program
196
169
  .requiredOption('--address <addr>', 'Signer address')
197
170
  .requiredOption('--message <msg>', 'Original message')
198
171
  .requiredOption('--signature <sig>', 'Signature to verify')
199
- .action(async (opts) => {
200
- const globalOpts = program.opts();
201
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
202
- try {
203
- const params = getCommonParams(globalOpts);
204
- const cid = params.connectId || '';
205
- const did = params.deviceId || '';
206
- let result;
207
- switch (opts.chain.toLowerCase()) {
208
- case 'evm':
209
- case 'eth':
210
- case 'ethereum':
211
- result = await sdk.evmVerifyMessage(cid, did, {
212
- address: opts.address,
213
- messageHex: opts.message,
214
- signature: opts.signature,
215
- });
216
- break;
217
- case 'btc':
218
- case 'bitcoin':
219
- result = await sdk.btcVerifyMessage(cid, did, {
220
- address: opts.address,
221
- message: opts.message,
222
- signature: opts.signature,
223
- coin: 'btc',
224
- });
225
- break;
226
- case 'starcoin':
227
- case 'stc':
228
- result = await sdk.starcoinVerifyMessage(cid, did, {
229
- publicKey: opts.address,
230
- message: opts.message,
231
- signature: opts.signature,
232
- });
233
- break;
234
- default:
235
- throw new Error(`verifyMessage not supported for chain: ${opts.chain}. Supported: evm, btc, starcoin`);
236
- }
237
- outputResult(globalOpts, result);
238
- }
239
- finally {
240
- sdk.dispose();
172
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
173
+ const cid = params.connectId || '';
174
+ const did = params.deviceId || '';
175
+ let result;
176
+ switch (opts.chain.toLowerCase()) {
177
+ case 'evm':
178
+ case 'eth':
179
+ case 'ethereum':
180
+ result = await sdk.evmVerifyMessage(cid, did, {
181
+ address: opts.address,
182
+ messageHex: opts.message,
183
+ signature: opts.signature,
184
+ ...params,
185
+ });
186
+ break;
187
+ case 'btc':
188
+ case 'bitcoin':
189
+ result = await sdk.btcVerifyMessage(cid, did, {
190
+ address: opts.address,
191
+ messageHex: opts.message,
192
+ signature: opts.signature,
193
+ coin: 'btc',
194
+ ...params,
195
+ });
196
+ break;
197
+ case 'starcoin':
198
+ case 'stc':
199
+ result = await sdk.starcoinVerifyMessage(cid, did, {
200
+ publicKey: opts.address,
201
+ messageHex: opts.message,
202
+ signature: opts.signature,
203
+ ...params,
204
+ });
205
+ break;
206
+ default:
207
+ throw new Error(`verifyMessage not supported for chain: ${opts.chain}. Supported: evm, btc, starcoin`);
241
208
  }
242
- });
209
+ outputResult(globalOpts, result);
210
+ }));
243
211
  program
244
212
  .command('batch-get-address')
245
213
  .description('Get addresses for multiple chains/paths in a single session')
246
214
  .requiredOption('--bundle <json>', 'JSON array of {chain, path, showOnDevice}')
247
- .action(async (opts) => {
248
- const globalOpts = program.opts();
249
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
250
- try {
251
- const bundle = safeJsonParse(opts.bundle, '--bundle');
252
- const result = await (0, chains_1.resolveBatchGetAddress)(sdk, {
253
- bundle,
254
- ...getCommonParams(globalOpts),
255
- });
256
- outputResult(globalOpts, result);
257
- }
258
- finally {
259
- sdk.dispose();
260
- }
261
- });
215
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
216
+ const bundle = safeJsonParse(opts.bundle, '--bundle');
217
+ const result = await (0, chains_1.resolveBatchGetAddress)(sdk, {
218
+ bundle,
219
+ ...params,
220
+ });
221
+ outputResult(globalOpts, result);
222
+ }));
262
223
  // ============================================================
263
224
  // Chain-Specific Commands
264
225
  // ============================================================
@@ -268,42 +229,28 @@ program
268
229
  .requiredOption('--domain-hash <hex>', 'EIP-712 domain separator hash')
269
230
  .requiredOption('--message-hash <hex>', 'EIP-712 message hash')
270
231
  .option('--path <path>', 'BIP44 derivation path', "m/44'/60'/0'/0/0")
271
- .action(async (opts) => {
272
- const globalOpts = program.opts();
273
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
274
- try {
275
- const p = getCommonParams(globalOpts);
276
- const result = await sdk.evmSignMessageEIP712(p.connectId || '', p.deviceId || '', {
277
- path: opts.path,
278
- domainHash: opts.domainHash,
279
- messageHash: opts.messageHash,
280
- });
281
- outputResult(globalOpts, result);
282
- }
283
- finally {
284
- sdk.dispose();
285
- }
286
- });
232
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
233
+ const result = await sdk.evmSignMessageEIP712(params.connectId || '', params.deviceId || '', {
234
+ path: opts.path,
235
+ domainHash: opts.domainHash,
236
+ messageHash: opts.messageHash,
237
+ ...params,
238
+ });
239
+ outputResult(globalOpts, result);
240
+ }));
287
241
  program
288
242
  .command('sol-sign-offchain')
289
243
  .description('Sign a Solana off-chain message (requires device confirmation)')
290
244
  .requiredOption('--message-hex <hex>', 'Off-chain message as hex')
291
245
  .option('--path <path>', 'BIP44 derivation path', "m/44'/501'/0'/0'")
292
- .action(async (opts) => {
293
- const globalOpts = program.opts();
294
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
295
- try {
296
- const p = getCommonParams(globalOpts);
297
- const result = await sdk.solSignOffchainMessage(p.connectId || '', p.deviceId || '', {
298
- path: opts.path,
299
- messageHex: opts.messageHex,
300
- });
301
- outputResult(globalOpts, result);
302
- }
303
- finally {
304
- sdk.dispose();
305
- }
306
- });
246
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
247
+ const result = await sdk.solSignOffchainMessage(params.connectId || '', params.deviceId || '', {
248
+ path: opts.path,
249
+ messageHex: opts.messageHex,
250
+ ...params,
251
+ });
252
+ outputResult(globalOpts, result);
253
+ }));
307
254
  program
308
255
  .command('nostr-encrypt')
309
256
  .description('Encrypt a message for a Nostr recipient')
@@ -311,23 +258,16 @@ program
311
258
  .requiredOption('--plaintext <text>', 'Message to encrypt')
312
259
  .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
313
260
  .option('--show-on-device <bool>', 'Display on device', 'false')
314
- .action(async (opts) => {
315
- const globalOpts = program.opts();
316
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
317
- try {
318
- const p = getCommonParams(globalOpts);
319
- const result = await sdk.nostrEncryptMessage(p.connectId || '', p.deviceId || '', {
320
- path: opts.path,
321
- pubkey: opts.pubkey,
322
- plaintext: opts.plaintext,
323
- showOnOneKey: opts.showOnDevice === 'true',
324
- });
325
- outputResult(globalOpts, result);
326
- }
327
- finally {
328
- sdk.dispose();
329
- }
330
- });
261
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
262
+ const result = await sdk.nostrEncryptMessage(params.connectId || '', params.deviceId || '', {
263
+ path: opts.path,
264
+ pubkey: opts.pubkey,
265
+ plaintext: opts.plaintext,
266
+ showOnOneKey: opts.showOnDevice === 'true',
267
+ ...params,
268
+ });
269
+ outputResult(globalOpts, result);
270
+ }));
331
271
  program
332
272
  .command('nostr-decrypt')
333
273
  .description('Decrypt a Nostr encrypted message')
@@ -335,105 +275,70 @@ program
335
275
  .requiredOption('--ciphertext <text>', 'Encrypted message')
336
276
  .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
337
277
  .option('--show-on-device <bool>', 'Display on device', 'false')
338
- .action(async (opts) => {
339
- const globalOpts = program.opts();
340
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
341
- try {
342
- const p = getCommonParams(globalOpts);
343
- const result = await sdk.nostrDecryptMessage(p.connectId || '', p.deviceId || '', {
344
- path: opts.path,
345
- pubkey: opts.pubkey,
346
- ciphertext: opts.ciphertext,
347
- showOnOneKey: opts.showOnDevice === 'true',
348
- });
349
- outputResult(globalOpts, result);
350
- }
351
- finally {
352
- sdk.dispose();
353
- }
354
- });
278
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
279
+ const result = await sdk.nostrDecryptMessage(params.connectId || '', params.deviceId || '', {
280
+ path: opts.path,
281
+ pubkey: opts.pubkey,
282
+ ciphertext: opts.ciphertext,
283
+ showOnOneKey: opts.showOnDevice === 'true',
284
+ ...params,
285
+ });
286
+ outputResult(globalOpts, result);
287
+ }));
355
288
  program
356
289
  .command('nostr-sign-schnorr')
357
290
  .description('Sign a Schnorr signature for Nostr')
358
291
  .requiredOption('--hash <hex>', 'Hash to sign (hex)')
359
292
  .option('--path <path>', 'BIP44 derivation path', "m/44'/1237'/0'/0/0")
360
- .action(async (opts) => {
361
- const globalOpts = program.opts();
362
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
363
- try {
364
- const p = getCommonParams(globalOpts);
365
- const result = await sdk.nostrSignSchnorr(p.connectId || '', p.deviceId || '', {
366
- path: opts.path,
367
- hash: opts.hash,
368
- });
369
- outputResult(globalOpts, result);
370
- }
371
- finally {
372
- sdk.dispose();
373
- }
374
- });
293
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
294
+ const result = await sdk.nostrSignSchnorr(params.connectId || '', params.deviceId || '', {
295
+ path: opts.path,
296
+ hash: opts.hash,
297
+ ...params,
298
+ });
299
+ outputResult(globalOpts, result);
300
+ }));
375
301
  program
376
302
  .command('lnurl-auth')
377
303
  .description('Authenticate with LNURL (Lightning Network)')
378
304
  .requiredOption('--domain <domain>', 'Service domain')
379
305
  .requiredOption('--k1 <hex>', 'Challenge k1 parameter')
380
- .action(async (opts) => {
381
- const globalOpts = program.opts();
382
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
383
- try {
384
- const p = getCommonParams(globalOpts);
385
- const result = await sdk.lnurlAuth(p.connectId || '', p.deviceId || '', {
386
- domain: opts.domain,
387
- k1: opts.k1,
388
- });
389
- outputResult(globalOpts, result);
390
- }
391
- finally {
392
- sdk.dispose();
393
- }
394
- });
306
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
307
+ const result = await sdk.lnurlAuth(params.connectId || '', params.deviceId || '', {
308
+ domain: opts.domain,
309
+ k1: opts.k1,
310
+ ...params,
311
+ });
312
+ outputResult(globalOpts, result);
313
+ }));
395
314
  program
396
315
  .command('conflux-sign-cip23')
397
316
  .description('Sign a Conflux CIP-23 structured message')
398
317
  .requiredOption('--domain-hash <hex>', 'CIP-23 domain hash')
399
318
  .requiredOption('--message-hash <hex>', 'CIP-23 message hash')
400
319
  .option('--path <path>', 'BIP44 derivation path', "m/44'/503'/0'/0/0")
401
- .action(async (opts) => {
402
- const globalOpts = program.opts();
403
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
404
- try {
405
- const p = getCommonParams(globalOpts);
406
- const result = await sdk.confluxSignMessageCIP23(p.connectId || '', p.deviceId || '', {
407
- path: opts.path,
408
- domainHash: opts.domainHash,
409
- messageHash: opts.messageHash,
410
- });
411
- outputResult(globalOpts, result);
412
- }
413
- finally {
414
- sdk.dispose();
415
- }
416
- });
320
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
321
+ const result = await sdk.confluxSignMessageCIP23(params.connectId || '', params.deviceId || '', {
322
+ path: opts.path,
323
+ domainHash: opts.domainHash,
324
+ messageHash: opts.messageHash,
325
+ ...params,
326
+ });
327
+ outputResult(globalOpts, result);
328
+ }));
417
329
  program
418
330
  .command('aptos-sign-in')
419
331
  .description('Sign an Aptos sign-in message')
420
332
  .requiredOption('--payload <text>', 'Sign-in payload string')
421
333
  .option('--path <path>', 'BIP44 derivation path', "m/44'/637'/0'/0'/0'")
422
- .action(async (opts) => {
423
- const globalOpts = program.opts();
424
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
425
- try {
426
- const p = getCommonParams(globalOpts);
427
- const result = await sdk.aptosSignInMessage(p.connectId || '', p.deviceId || '', {
428
- path: opts.path,
429
- payload: opts.payload,
430
- });
431
- outputResult(globalOpts, result);
432
- }
433
- finally {
434
- sdk.dispose();
435
- }
436
- });
334
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
335
+ const result = await sdk.aptosSignInMessage(params.connectId || '', params.deviceId || '', {
336
+ path: opts.path,
337
+ payload: opts.payload,
338
+ ...params,
339
+ });
340
+ outputResult(globalOpts, result);
341
+ }));
437
342
  program
438
343
  .command('ton-sign-proof')
439
344
  .description('Sign a TON proof (for wallet authentication)')
@@ -441,92 +346,60 @@ program
441
346
  .requiredOption('--expire-at <timestamp>', 'Proof expiration timestamp')
442
347
  .option('--comment <text>', 'Optional comment')
443
348
  .option('--path <path>', 'BIP44 derivation path', "m/44'/607'/0'")
444
- .action(async (opts) => {
445
- const globalOpts = program.opts();
446
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
447
- try {
448
- const p = getCommonParams(globalOpts);
449
- const result = await sdk.tonSignProof(p.connectId || '', p.deviceId || '', {
450
- path: opts.path,
451
- appdomain: opts.appdomain,
452
- expireAt: safeParseInt(opts.expireAt, '--expire-at'),
453
- ...(opts.comment ? { comment: opts.comment } : {}),
454
- });
455
- outputResult(globalOpts, result);
456
- }
457
- finally {
458
- sdk.dispose();
459
- }
460
- });
349
+ .action(opts => runCommand({ needsSession: true }, async ({ sdk, globalOpts, params }) => {
350
+ const result = await sdk.tonSignProof(params.connectId || '', params.deviceId || '', {
351
+ path: opts.path,
352
+ appdomain: opts.appdomain,
353
+ expireAt: safeParseInt(opts.expireAt, '--expire-at'),
354
+ ...(opts.comment ? { comment: opts.comment } : {}),
355
+ ...params,
356
+ });
357
+ outputResult(globalOpts, result);
358
+ }));
461
359
  // ============================================================
462
360
  // Firmware Commands
463
361
  // ============================================================
464
362
  program
465
363
  .command('firmware-check')
466
364
  .description('Check if firmware updates are available')
467
- .action(async () => {
468
- const globalOpts = program.opts();
469
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
470
- try {
471
- const result = await sdk.checkFirmwareRelease(globalOpts.connectId);
472
- outputResult(globalOpts, result);
473
- }
474
- finally {
475
- sdk.dispose();
476
- }
477
- });
365
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
366
+ const result = await sdk.checkFirmwareRelease(globalOpts.connectId);
367
+ outputResult(globalOpts, result);
368
+ }));
478
369
  program
479
370
  .command('firmware-check-all')
480
371
  .description('Check all firmware components (system, BLE, bootloader)')
481
- .action(async () => {
482
- const globalOpts = program.opts();
483
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
484
- try {
485
- const result = await sdk.checkAllFirmwareRelease(globalOpts.connectId);
486
- outputResult(globalOpts, result);
487
- }
488
- finally {
489
- sdk.dispose();
490
- }
491
- });
372
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
373
+ const result = await sdk.checkAllFirmwareRelease(globalOpts.connectId);
374
+ outputResult(globalOpts, result);
375
+ }));
492
376
  program
493
377
  .command('firmware-update')
494
378
  .description('Firmware update is not supported via CLI')
495
- .action(() => {
496
- outputResult(program.opts(), {
497
- success: false,
498
- payload: {
499
- error: 'Firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
500
- code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
501
- },
502
- });
503
- });
379
+ .action(() => respondAndExit({
380
+ success: false,
381
+ payload: {
382
+ error: 'Firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
383
+ code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
384
+ },
385
+ }));
504
386
  program
505
387
  .command('firmware-update-ble')
506
388
  .description('BLE firmware update is not supported via CLI')
507
- .action(() => {
508
- outputResult(program.opts(), {
509
- success: false,
510
- payload: {
511
- error: 'BLE firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
512
- code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
513
- },
514
- });
515
- });
389
+ .action(() => respondAndExit({
390
+ success: false,
391
+ payload: {
392
+ error: 'BLE firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
393
+ code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
394
+ },
395
+ }));
516
396
  program
517
397
  .command('bootloader-check')
518
398
  .description('Check bootloader version and status')
519
- .action(async () => {
520
- const globalOpts = program.opts();
521
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
522
- try {
523
- const result = await sdk.checkBootloaderRelease(globalOpts.connectId);
524
- outputResult(globalOpts, result);
525
- }
526
- finally {
527
- sdk.dispose();
528
- }
529
- });
399
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
400
+ const result = await sdk.checkBootloaderRelease(globalOpts.connectId);
401
+ outputResult(globalOpts, result);
402
+ }));
530
403
  // ============================================================
531
404
  // Security / Management Commands
532
405
  // ============================================================
@@ -534,65 +407,50 @@ program
534
407
  .command('change-pin')
535
408
  .description('Change or set the device PIN code')
536
409
  .option('--remove', 'Remove PIN protection instead of changing')
537
- .action(async (opts) => {
538
- const globalOpts = program.opts();
539
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
540
- try {
541
- const result = await sdk.deviceChangePin(globalOpts.connectId, {
542
- remove: opts.remove ?? false,
543
- });
544
- outputResult(globalOpts, result);
545
- }
546
- finally {
547
- sdk.dispose();
548
- }
549
- });
410
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
411
+ const result = await sdk.deviceChangePin(globalOpts.connectId, {
412
+ remove: opts.remove ?? false,
413
+ });
414
+ outputResult(globalOpts, result);
415
+ }));
550
416
  program
551
417
  .command('passphrase-state')
552
418
  .description('Get current passphrase state (for hidden wallet session management)')
553
- .action(async () => {
554
- const globalOpts = program.opts();
555
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
556
- try {
557
- const result = await sdk.getPassphraseState(globalOpts.connectId, {
558
- useEmptyPassphrase: globalOpts.useEmptyPassphrase,
559
- });
560
- outputResult(globalOpts, result);
561
- }
562
- finally {
563
- sdk.dispose();
564
- }
565
- });
419
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
420
+ const result = await sdk.getPassphraseState(globalOpts.connectId, {
421
+ useEmptyPassphrase: globalOpts.useEmptyPassphrase,
422
+ });
423
+ outputResult(globalOpts, result);
424
+ }));
566
425
  program
567
426
  .command('toggle-passphrase')
568
427
  .description('Enable or disable passphrase (hidden wallet) protection')
569
428
  .requiredOption('--enable <bool>', 'true to enable, false to disable')
570
- .action(async (opts) => {
571
- const globalOpts = program.opts();
572
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
573
- try {
574
- const result = await sdk.deviceSettings(globalOpts.connectId, {
575
- usePassphrase: opts.enable === 'true',
576
- });
577
- outputResult(globalOpts, result);
578
- }
579
- finally {
580
- sdk.dispose();
581
- }
582
- });
429
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
430
+ const result = await sdk.deviceSettings(globalOpts.connectId, {
431
+ usePassphrase: opts.enable === 'true',
432
+ });
433
+ outputResult(globalOpts, result);
434
+ }));
583
435
  program
584
436
  .command('device-wipe')
585
- .description('Factory reset — erase ALL data (IRREVERSIBLE)')
586
- .action(async () => {
587
- const globalOpts = program.opts();
588
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
589
- try {
437
+ .description('Factory reset — erase ALL data (IRREVERSIBLE, requires --yes)')
438
+ .option('--yes', 'Confirm factory reset (required)')
439
+ .action(opts => {
440
+ if (!opts.yes) {
441
+ respondAndExit({
442
+ success: false,
443
+ payload: {
444
+ error: 'Factory reset requires --yes flag to confirm. This operation is IRREVERSIBLE.',
445
+ code: 'CONFIRMATION_REQUIRED',
446
+ },
447
+ });
448
+ return;
449
+ }
450
+ return runCommand({}, async ({ sdk, globalOpts }) => {
590
451
  const result = await sdk.deviceWipe(globalOpts.connectId);
591
452
  outputResult(globalOpts, result);
592
- }
593
- finally {
594
- sdk.dispose();
595
- }
453
+ });
596
454
  });
597
455
  program
598
456
  .command('device-settings')
@@ -603,110 +461,371 @@ program
603
461
  .option('--passphrase-always-on-device <bool>', 'Always enter passphrase on device')
604
462
  .option('--haptic-feedback <bool>', 'Enable/disable haptic feedback')
605
463
  .option('--auto-shutdown-delay <seconds>', 'Auto shutdown timeout in seconds')
606
- .action(async (opts) => {
607
- const globalOpts = program.opts();
608
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
609
- try {
610
- // Map CLI options to SDK param names (camelCase snake_case handled by SDK)
611
- // Reference: packages/core/src/api/device/DeviceSettings.ts
612
- const settings = {};
613
- if (opts.label !== undefined)
614
- settings.label = opts.label;
615
- if (opts.autoLockDelay !== undefined)
616
- settings.autoLockDelayMs = safeParseInt(opts.autoLockDelay, '--auto-lock-delay') * 1000;
617
- if (opts.language !== undefined)
618
- settings.language = opts.language;
619
- if (opts.passphraseAlwaysOnDevice !== undefined)
620
- settings.passphraseAlwaysOnDevice = opts.passphraseAlwaysOnDevice === 'true';
621
- if (opts.hapticFeedback !== undefined)
622
- settings.hapticFeedback = opts.hapticFeedback === 'true';
623
- if (opts.autoShutdownDelay !== undefined)
624
- settings.autoShutdownDelayMs =
625
- safeParseInt(opts.autoShutdownDelay, '--auto-shutdown-delay') * 1000;
626
- if (Object.keys(settings).length === 0) {
627
- outputResult(globalOpts, {
628
- success: false,
629
- error: 'No settings provided. Use --label, --auto-lock-delay, --language, etc.',
630
- });
631
- return;
632
- }
633
- const result = await sdk.deviceSettings(globalOpts.connectId, settings);
634
- outputResult(globalOpts, result);
635
- }
636
- finally {
637
- sdk.dispose();
464
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
465
+ // Map CLI options to SDK param names (camelCase → snake_case handled by SDK)
466
+ // Reference: packages/core/src/api/device/DeviceSettings.ts
467
+ const settings = {};
468
+ if (opts.label !== undefined)
469
+ settings.label = opts.label;
470
+ if (opts.autoLockDelay !== undefined)
471
+ settings.autoLockDelayMs = safeParseInt(opts.autoLockDelay, '--auto-lock-delay') * 1000;
472
+ if (opts.language !== undefined)
473
+ settings.language = opts.language;
474
+ if (opts.passphraseAlwaysOnDevice !== undefined)
475
+ settings.passphraseAlwaysOnDevice = opts.passphraseAlwaysOnDevice === 'true';
476
+ if (opts.hapticFeedback !== undefined)
477
+ settings.hapticFeedback = opts.hapticFeedback === 'true';
478
+ if (opts.autoShutdownDelay !== undefined)
479
+ settings.autoShutdownDelayMs =
480
+ safeParseInt(opts.autoShutdownDelay, '--auto-shutdown-delay') * 1000;
481
+ if (Object.keys(settings).length === 0) {
482
+ outputResult(globalOpts, {
483
+ success: false,
484
+ error: 'No settings provided. Use --label, --auto-lock-delay, --language, etc.',
485
+ });
486
+ return;
638
487
  }
639
- });
488
+ const result = await sdk.deviceSettings(globalOpts.connectId, settings);
489
+ outputResult(globalOpts, result);
490
+ }));
640
491
  program
641
492
  .command('device-verify')
642
493
  .description('Verify device is genuine OneKey hardware')
643
- .action(async () => {
644
- const globalOpts = program.opts();
645
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
646
- try {
647
- const result = await sdk.deviceVerify(globalOpts.connectId);
648
- outputResult(globalOpts, result);
649
- }
650
- finally {
651
- sdk.dispose();
652
- }
653
- });
494
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
495
+ const result = await sdk.deviceVerify(globalOpts.connectId, { dataHex: '' });
496
+ outputResult(globalOpts, result);
497
+ }));
654
498
  program
655
499
  .command('lock')
656
500
  .description('Lock the device')
657
- .action(async () => {
658
- const globalOpts = program.opts();
659
- const sdk = await (0, sdk_1.createSDK)(globalOpts);
660
- try {
661
- const result = await sdk.deviceLock(globalOpts.connectId);
662
- outputResult(globalOpts, result);
663
- }
664
- finally {
665
- sdk.dispose();
666
- }
667
- });
501
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
502
+ const result = await sdk.deviceLock(globalOpts.connectId, {});
503
+ outputResult(globalOpts, result);
504
+ }));
505
+ // ============================================================
506
+ // Session Management Commands
507
+ // ============================================================
508
+ const sessionCmd = program.command('session').description('Manage device passphrase session cache');
509
+ sessionCmd
510
+ .command('connect')
511
+ .description('Connect device and establish passphrase session (cached for subsequent commands)')
512
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
513
+ // 1. Search for device
514
+ const searchResult = await sdk.searchDevices();
515
+ if (!searchResult?.success || !searchResult.payload?.length) {
516
+ outputResult(globalOpts, {
517
+ success: false,
518
+ payload: { error: 'No device found', code: 'NO_DEVICE' },
519
+ });
520
+ return;
521
+ }
522
+ const device = searchResult.payload[0];
523
+ const connectId = device.connectId || globalOpts.connectId;
524
+ // 2. Unlock if locked — getPassphraseState below talks to a live
525
+ // device session, which a locked device will reject with an obscure
526
+ // error. Uses the same PinInvalid retry policy as prepareSession.
527
+ if (device.features?.unlocked === false) {
528
+ process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
529
+ await unlockWithRetry(sdk, connectId);
530
+ }
531
+ // 3. Get passphraseState (triggers 1/2/3 selection)
532
+ const psResult = await sdk.getPassphraseState(connectId, {
533
+ initSession: true,
534
+ useEmptyPassphrase: false,
535
+ });
536
+ if (!psResult.success) {
537
+ outputResult(globalOpts, psResult);
538
+ return;
539
+ }
540
+ const passphraseState = psResult.payload;
541
+ // 4. Get address to verify + extract deviceId
542
+ const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
543
+ path: "m/44'/60'/0'/0/0",
544
+ showOnOneKey: false,
545
+ passphraseState,
546
+ });
547
+ // 5. Fetch the now-active session_id via getFeatures.
548
+ //
549
+ // IMPORTANT: pass `passphraseState` here. Without it, the SDK's
550
+ // connectStateChange guard (core/index.ts) would see the payload's
551
+ // passphraseState flip from mnNy → undefined, clear the cached Device,
552
+ // and call Initialize again with no passphrase_state / no session_id.
553
+ // That Initialize resets the device to the standard wallet and returns
554
+ // a *standard-wallet* session_id — which we'd then save in the keychain
555
+ // paired with the hidden-wallet passphraseState. On the next CLI run
556
+ // the mismatch would trigger PassphraseRequest (1/2/3 again).
557
+ const featResult = await sdk.getFeatures(connectId, {
558
+ passphraseState,
559
+ skipPassphraseCheck: true,
560
+ });
561
+ const featPayload = featResult?.success ? featResult.payload : undefined;
562
+ const deviceId = featPayload?.device_id || device.deviceId || '';
563
+ const sessionId = featPayload?.session_id || '';
564
+ // 6. Save to keychain
565
+ if (passphraseState && deviceId && sessionId) {
566
+ await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
567
+ }
568
+ outputResult(globalOpts, {
569
+ success: true,
570
+ payload: {
571
+ passphraseState,
572
+ deviceId,
573
+ ...(sessionId ? { sessionId } : {}),
574
+ ...(addrResult?.success ? { address: addrResult.payload.address } : {}),
575
+ },
576
+ });
577
+ }));
578
+ sessionCmd
579
+ .command('disconnect')
580
+ .description('Clear cached device session')
581
+ .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
582
+ const searchResult = await sdk.searchDevices();
583
+ const device = // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
584
+ searchResult?.payload?.[0];
585
+ const deviceId = device?.features?.device_id || device?.deviceId;
586
+ if (deviceId) {
587
+ await (0, session_1.clearSessionFromKeychain)(deviceId);
588
+ }
589
+ outputResult(globalOpts, {
590
+ success: true,
591
+ payload: deviceId ? { deviceId } : {},
592
+ });
593
+ }));
668
594
  // ============================================================
669
595
  // Helpers
670
596
  // ============================================================
671
597
  /**
672
598
  * Extract common device params from global CLI options.
673
599
  * These are passed to every SDK method call.
600
+ *
601
+ * skipPassphraseCheck is always true because:
602
+ * - prepareSession handles passphrase selection (1/2/3 + pinentry/device)
603
+ * - Without it, SDK's checkPassphraseStateSafety triggers a SECOND
604
+ * REQUEST_PASSPHRASE (double prompt) even when passphraseState is set
605
+ * - Error 114 (no passphrase state) is also bypassed — our interactive
606
+ * handler in REQUEST_PASSPHRASE handles it correctly
674
607
  */
608
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
675
609
  function getCommonParams(globalOpts) {
676
610
  return {
677
611
  connectId: globalOpts.connectId,
678
612
  deviceId: globalOpts.deviceId,
679
613
  passphraseState: globalOpts.passphraseState,
680
614
  useEmptyPassphrase: globalOpts.useEmptyPassphrase,
615
+ skipPassphraseCheck: true,
681
616
  };
682
617
  }
618
+ /**
619
+ * HardwareErrorCode.PinInvalid from @onekeyfe/hd-shared. Duplicated here
620
+ * to avoid pulling the shared package's runtime just for one enum value.
621
+ * (PinCancelled is 802 — we don't retry it, any non-PinInvalid failure
622
+ * falls through to the "bail out" branch below.)
623
+ */
624
+ const HW_ERR_PIN_INVALID = 801;
625
+ /**
626
+ * Unlock a locked device, retrying up to `maxAttempts` times on
627
+ * `PinInvalid`. On `PinCancelled` or any other failure, throws immediately
628
+ * so the structured error surfaces to the user via runCommand's catch.
629
+ *
630
+ * With `@@ONEKEY_INPUT_PIN_IN_DEVICE` semantics the device firmware
631
+ * normally loops PIN entry internally and only reports back on success
632
+ * or cancel. Retrying here is defensive: if the device ever surfaces
633
+ * `PinInvalid` directly (older firmware, specific models), give the
634
+ * user another chance without forcing a re-run of the whole command.
635
+ */
636
+ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
637
+ let lastPayload = {};
638
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
639
+ const result = await sdk.deviceUnlock(connectId, {});
640
+ if (result?.success && result.payload) {
641
+ return { payload: result.payload };
642
+ }
643
+ lastPayload = (result?.payload ?? {});
644
+ const { code } = lastPayload;
645
+ const isPinInvalid = code === HW_ERR_PIN_INVALID;
646
+ if (!isPinInvalid || attempt >= maxAttempts) {
647
+ // PinCancelled, other error, or PinInvalid on the last attempt — bail out
648
+ const err = new Error(`Device unlock failed: ${lastPayload.error ?? 'unknown error'}`);
649
+ err.code = code ?? 'UNLOCK_FAILED';
650
+ throw err;
651
+ }
652
+ process.stderr.write(`[onekey-hw] Invalid PIN. Retry on your device (attempt ${attempt + 1}/${maxAttempts}).\n`);
653
+ }
654
+ // Unreachable — the loop either returns or throws
655
+ throw new Error('Device unlock failed: retry loop exited without a result');
656
+ }
657
+ /**
658
+ * Prepare passphrase session before SDK calls.
659
+ *
660
+ * 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
661
+ * 2. Try keychain → preloadSessionCache → use cached session
662
+ * 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
663
+ *
664
+ * After this, globalOpts.passphraseState is set and getCommonParams will include it.
665
+ */
666
+ async function prepareSession(sdk,
667
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
668
+ globalOpts) {
669
+ // Skip if standard wallet or passphraseState already provided
670
+ if (globalOpts.useEmptyPassphrase || globalOpts.passphraseState) {
671
+ return globalOpts.passphraseState;
672
+ }
673
+ // Errors from the SDK calls below (PIN cancelled, transport broken,
674
+ // getPassphraseState rejection) intentionally propagate to runCommand's
675
+ // catch block, which renders them as structured `{ success: false,
676
+ // payload: { error, code } }` output instead of silently falling through
677
+ // to a confusing downstream error 112 / 114.
678
+ // ── Step 1: Discover device ──────────────────────────────────────
679
+ const searchResult = await sdk.searchDevices();
680
+ if (!searchResult?.success ||
681
+ !Array.isArray(searchResult.payload) ||
682
+ searchResult.payload.length === 0) {
683
+ return undefined;
684
+ }
685
+ const device = searchResult.payload[0];
686
+ const connectId = device.connectId || globalOpts.connectId || '';
687
+ if (!globalOpts.connectId && connectId) {
688
+ globalOpts.connectId = connectId;
689
+ }
690
+ // ── Step 2: Get features if searchDevices didn't populate them ──
691
+ // getFeatures failures here are non-fatal — we fall through to Step 3
692
+ // which will fail with a clearer error if the device is truly unreachable.
693
+ let deviceId = device.features?.device_id || device.deviceId || '';
694
+ let unlocked = device.features?.unlocked;
695
+ let passphraseProtection = device.features?.passphrase_protection;
696
+ if (!deviceId || unlocked == null || passphraseProtection == null) {
697
+ try {
698
+ const featResult = await sdk.getFeatures(connectId);
699
+ if (featResult?.success && featResult.payload) {
700
+ deviceId = featResult.payload.device_id || deviceId;
701
+ unlocked = featResult.payload.unlocked;
702
+ passphraseProtection = featResult.payload.passphrase_protection;
703
+ }
704
+ }
705
+ catch {
706
+ /* non-fatal — Step 3 will surface a clear error if device is gone */
707
+ }
708
+ }
709
+ // ── Step 3: Unlock if locked (matches app-monorepo ServiceHardware flow) ──
710
+ // Track whether device was locked — locking invalidates passphrase sessions,
711
+ // so keychain session reuse is only possible if device was already unlocked.
712
+ // PinInvalid retries inside unlockWithRetry; PinCancelled/other failures
713
+ // throw and propagate to runCommand's catch for structured output.
714
+ const wasLocked = unlocked === false;
715
+ if (wasLocked) {
716
+ process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
717
+ const { payload: feat } = await unlockWithRetry(sdk, connectId);
718
+ deviceId = feat.device_id || deviceId;
719
+ unlocked = feat.unlocked;
720
+ passphraseProtection = feat.passphrase_protection;
721
+ }
722
+ if (!globalOpts.deviceId && deviceId) {
723
+ globalOpts.deviceId = deviceId;
724
+ }
725
+ // ── Step 4: Check passphrase protection ──────────────────────────
726
+ if (passphraseProtection === false) {
727
+ return undefined;
728
+ }
729
+ // ── Step 5: Try keychain session reuse ───────────────────────────
730
+ // Only attempt if device was already unlocked — locking invalidates
731
+ // all passphrase sessions, so cached session_id is useless after unlock.
732
+ if (!wasLocked && deviceId) {
733
+ const cached = await (0, session_1.preloadSessionFromKeychain)(deviceId);
734
+ if (cached) {
735
+ globalOpts.passphraseState = cached;
736
+ return cached;
737
+ }
738
+ }
739
+ // ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
740
+ const psResult = await sdk.getPassphraseState(connectId, {
741
+ initSession: true,
742
+ useEmptyPassphrase: false,
743
+ });
744
+ if (psResult.success && psResult.payload) {
745
+ const passphraseState = psResult.payload;
746
+ globalOpts.passphraseState = passphraseState;
747
+ // Save session to keychain for next invocation.
748
+ //
749
+ // Pass passphraseState to keep connectStateChange=false — otherwise
750
+ // Initialize would be re-run without passphrase_state, resetting the
751
+ // device to the standard wallet and returning a mismatched session_id.
752
+ // See the matching comment in `session connect`.
753
+ if (deviceId) {
754
+ const featAfter = await sdk.getFeatures(connectId, {
755
+ passphraseState,
756
+ skipPassphraseCheck: true,
757
+ });
758
+ const sessionId = featAfter?.success ? featAfter.payload?.session_id : undefined;
759
+ if (sessionId) {
760
+ await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
761
+ await (0, session_1.preloadSessionFromKeychain)(deviceId);
762
+ }
763
+ }
764
+ return passphraseState;
765
+ }
766
+ return undefined;
767
+ }
768
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
683
769
  function outputResult(_globalOpts, result) {
684
- // #10 FIX: Always use JSON.stringify to avoid [Object] truncation
685
770
  console.log(JSON.stringify(result, null, 2));
686
- // Exit after output — SDK event listeners keep the process alive otherwise
687
- if (result && typeof result === 'object' && 'success' in result && !result.success) {
688
- process.exit(1);
771
+ if (result &&
772
+ typeof result === 'object' &&
773
+ 'success' in result &&
774
+ !result.success) {
775
+ process.exitCode = 1;
776
+ }
777
+ // No process.exit here — runCommand() below handles dispose + exit so SDK
778
+ // async cleanup (USB release, event listener teardown) finishes first.
779
+ }
780
+ async function runCommand(options, handler) {
781
+ const globalOpts = program.opts();
782
+ try {
783
+ const sdk = await (0, sdk_1.createSDK)(globalOpts);
784
+ if (options.needsSession) {
785
+ await prepareSession(sdk, globalOpts);
786
+ }
787
+ await handler({
788
+ sdk,
789
+ globalOpts,
790
+ params: getCommonParams(globalOpts),
791
+ });
689
792
  }
690
- else {
691
- process.exit(0);
793
+ catch (err) {
794
+ const code = err.code || 'COMMAND_FAILED';
795
+ outputResult(globalOpts, {
796
+ success: false,
797
+ payload: {
798
+ error: err instanceof Error ? err.message : String(err),
799
+ code,
800
+ },
801
+ });
802
+ }
803
+ finally {
804
+ // Singleton disposer — awaits the SDK, calls dispose, clears the
805
+ // promise reference. Idempotent, safe to call even if init failed.
806
+ await (0, sdk_1.disposeSDK)();
692
807
  }
808
+ // SDK event listeners can keep the event loop alive after dispose.
809
+ // setImmediate lets any trailing stdout/stderr writes flush first.
810
+ setImmediate(() => process.exit(process.exitCode ?? 0));
811
+ }
812
+ /** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
813
+ function respondAndExit(result) {
814
+ outputResult({}, result);
815
+ setImmediate(() => process.exit(process.exitCode ?? 0));
693
816
  }
694
817
  /**
695
- * #6 FIX: Safe JSON.parse with structured error output
818
+ * Safe JSON.parse. Throws on failure — runCommand() catches and renders
819
+ * the structured error so the SDK still gets disposed cleanly.
696
820
  */
697
821
  function safeJsonParse(input, label) {
698
822
  try {
699
823
  return JSON.parse(input);
700
824
  }
701
825
  catch {
702
- console.error(JSON.stringify({
703
- success: false,
704
- payload: {
705
- error: `Invalid JSON for ${label}: ${input.slice(0, 100)}`,
706
- code: 'INVALID_JSON',
707
- },
708
- }));
709
- process.exit(1);
826
+ const err = new Error(`Invalid JSON for ${label}: ${input.slice(0, 100)}`);
827
+ err.code = 'INVALID_JSON';
828
+ throw err;
710
829
  }
711
830
  }
712
831
  /**