@onekeyfe/hardware-cli 1.1.25-alpha.1 → 1.1.26-alpha.10

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 (45) 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/pinentry.d.ts +24 -0
  8. package/dist/pinentry.js +118 -0
  9. package/dist/sdk.d.ts +15 -5
  10. package/dist/sdk.js +175 -131
  11. package/dist/session.d.ts +22 -0
  12. package/dist/session.js +83 -0
  13. package/dist/storage/index.d.ts +2 -0
  14. package/dist/storage/index.js +5 -0
  15. package/dist/storage/process-utils.d.ts +2 -0
  16. package/dist/storage/process-utils.js +44 -0
  17. package/dist/storage/secure-storage.linux.d.ts +11 -0
  18. package/dist/storage/secure-storage.linux.js +59 -0
  19. package/dist/storage/secure-storage.macos.d.ts +11 -0
  20. package/dist/storage/secure-storage.macos.js +65 -0
  21. package/dist/storage/storage-factory.d.ts +3 -0
  22. package/dist/storage/storage-factory.js +14 -0
  23. package/dist/storage/types.d.ts +18 -0
  24. package/dist/storage/types.js +2 -0
  25. package/package.json +17 -14
  26. package/src/__tests__/pinentry.test.ts +185 -0
  27. package/src/chains.ts +229 -85
  28. package/src/cli.ts +620 -297
  29. package/src/pinentry.ts +146 -0
  30. package/src/sdk.ts +161 -125
  31. package/src/session.ts +89 -0
  32. package/src/storage/index.ts +2 -0
  33. package/src/storage/process-utils.ts +50 -0
  34. package/src/storage/secure-storage.linux.ts +68 -0
  35. package/src/storage/secure-storage.macos.ts +68 -0
  36. package/src/storage/storage-factory.ts +13 -0
  37. package/src/storage/types.ts +17 -0
  38. package/tsconfig.json +5 -7
  39. package/.claude-plugin/plugin.json +0 -14
  40. package/AGENTS.md +0 -40
  41. package/CLAUDE.md +0 -40
  42. package/README.md +0 -112
  43. package/evals/cases.json +0 -373
  44. package/evals/run-evals.sh +0 -136
  45. package/rollup.config.js +0 -28
package/dist/index.d.ts CHANGED
@@ -1,95 +1,22 @@
1
- import * as _onekeyfe_hd_core from '@onekeyfe/hd-core';
2
- import { CoreApi } from '@onekeyfe/hd-core';
3
-
4
1
  /**
5
- * SDK Factory — creates and initializes the hardware SDK instance
6
- * for CLI usage with the appropriate transport.
2
+ * @onekeyfe/hardware-cli
7
3
  *
8
- * CRITICAL: Must register UI event handlers for PIN, Passphrase, and Button
9
- * confirmation. Without these, the SDK will hang waiting for responses.
4
+ * OneKey hardware wallet CLI for AI agent integration.
5
+ * Provides device management, multi-chain signing, firmware updates,
6
+ * and security management capabilities.
10
7
  *
11
- * Reference: packages/core/src/core/index.ts (event registration pattern)
12
- */
13
- interface SDKOptions {
14
- connectId?: string;
15
- passphraseState?: string;
16
- useEmptyPassphrase?: boolean;
17
- }
18
- declare function createSDK(opts: SDKOptions): Promise<_onekeyfe_hd_core.CoreApi>;
19
-
20
- /**
21
- * Chain Resolver — maps chain identifiers to the correct SDK API calls.
22
- * Handles derivation path defaults and chain-specific parameter transformations.
8
+ * Usage:
9
+ * npx @onekeyfe/hardware-cli search
10
+ * npx @onekeyfe/hardware-cli get-address --chain evm
11
+ * npx @onekeyfe/hardware-cli sign-transaction --chain evm --tx '{...}'
23
12
  *
24
- * Reference: developer-portal docs at
25
- * content/en/hardware-sdk/chains/<chain>/<method>.mdx
26
- * content/en/hardware-sdk/core-api-guide.mdx (HD path section)
13
+ * All output is structured JSON for AI agent consumption.
27
14
  *
28
- * Type definitions: packages/core/src/types/api/*.ts
29
- */
30
-
31
- /**
32
- * Common params passed to all SDK methods.
33
- * Reference: packages/core/src/types/api/export.ts (CommonParams)
15
+ * IMPORTANT: All signing operations require physical confirmation on the
16
+ * hardware device. The CLI handles PIN/Passphrase prompts via stdin for
17
+ * interactive use, or via SDK event system for programmatic use.
34
18
  */
35
- interface CommonCLIParams {
36
- connectId?: string;
37
- deviceId?: string;
38
- passphraseState?: string;
39
- useEmptyPassphrase?: boolean;
40
- }
41
- interface GetAddressParams extends CommonCLIParams {
42
- chain: string;
43
- path?: string;
44
- showOnDevice?: boolean;
45
- }
46
- declare function resolveGetAddress(sdk: CoreApi, params: GetAddressParams): Promise<{
47
- success: boolean;
48
- error: string;
49
- chain: string;
50
- path: string;
51
- } | {
52
- chain: string;
53
- path: string;
54
- success?: undefined;
55
- error?: undefined;
56
- }>;
57
- interface GetPublicKeyParams extends CommonCLIParams {
58
- chain: string;
59
- path?: string;
60
- }
61
- declare function resolveGetPublicKey(sdk: CoreApi, params: GetPublicKeyParams): Promise<{
62
- chain: string;
63
- path: string;
64
- }>;
65
- interface SignTransactionParams extends CommonCLIParams {
66
- chain: string;
67
- path?: string;
68
- transaction: Record<string, unknown>;
69
- }
70
- declare function resolveSignTransaction(sdk: CoreApi, params: SignTransactionParams): Promise<{
71
- chain: string;
72
- path: string;
73
- }>;
74
- interface SignMessageParams extends CommonCLIParams {
75
- chain: string;
76
- path?: string;
77
- message: string;
78
- }
79
- declare function resolveSignMessage(sdk: CoreApi, params: SignMessageParams): Promise<{
80
- chain: string;
81
- path: string;
82
- }>;
83
- interface BatchGetAddressParams extends CommonCLIParams {
84
- bundle: Array<{
85
- chain: string;
86
- path?: string;
87
- showOnDevice?: boolean;
88
- }>;
89
- }
90
- declare function resolveBatchGetAddress(sdk: CoreApi, params: BatchGetAddressParams): Promise<{
91
- success: boolean;
92
- addresses: Record<string, unknown>[];
93
- }>;
94
-
95
- export { BatchGetAddressParams, CommonCLIParams, GetAddressParams, GetPublicKeyParams, SDKOptions, SignMessageParams, SignTransactionParams, createSDK, resolveBatchGetAddress, resolveGetAddress, resolveGetPublicKey, resolveSignMessage, resolveSignTransaction };
19
+ export { createSDK } from './sdk';
20
+ export type { SDKOptions } from './sdk';
21
+ export { resolveGetAddress, resolveGetPublicKey, resolveSignTransaction, resolveSignMessage, resolveBatchGetAddress, } from './chains';
22
+ export type { CommonCLIParams, GetAddressParams, GetPublicKeyParams, SignTransactionParams, SignMessageParams, BatchGetAddressParams, } from './chains';
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
- 'use strict';
2
-
1
+ "use strict";
3
2
  /**
4
3
  * @onekeyfe/hardware-cli
5
4
  *
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Secure passphrase input via the system pinentry program
3
+ * (pinentry-mac / pinentry / pinentry-gnome3 / pinentry-qt).
4
+ *
5
+ * The passphrase is entered in an OS dialog and returned as a string —
6
+ * it never appears in the terminal, shell history, process arguments,
7
+ * or env vars. Commands are piped over stdin so the dialog configuration
8
+ * (description, prompt) doesn't show up in argv either.
9
+ *
10
+ * Ported from app-monorepo apps/cli/src/utils/pinentry.ts; kept as a
11
+ * standalone module so the pure parser functions can be unit-tested
12
+ * without touching the SDK, child processes, or hardware.
13
+ */
14
+ export declare function decodeAssuanData(encoded: string): string;
15
+ export declare function parsePinentryStdout(stdout: string): {
16
+ data?: string;
17
+ cancelled: boolean;
18
+ };
19
+ export declare function findPinentry(): string | null;
20
+ export interface PinentryResult {
21
+ value: string;
22
+ passphraseOnDevice: boolean;
23
+ }
24
+ export declare function promptPassphraseViaPinentry(): Promise<PinentryResult>;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * Secure passphrase input via the system pinentry program
4
+ * (pinentry-mac / pinentry / pinentry-gnome3 / pinentry-qt).
5
+ *
6
+ * The passphrase is entered in an OS dialog and returned as a string —
7
+ * it never appears in the terminal, shell history, process arguments,
8
+ * or env vars. Commands are piped over stdin so the dialog configuration
9
+ * (description, prompt) doesn't show up in argv either.
10
+ *
11
+ * Ported from app-monorepo apps/cli/src/utils/pinentry.ts; kept as a
12
+ * standalone module so the pure parser functions can be unit-tested
13
+ * without touching the SDK, child processes, or hardware.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.promptPassphraseViaPinentry = exports.findPinentry = exports.parsePinentryStdout = exports.decodeAssuanData = void 0;
17
+ const node_child_process_1 = require("node:child_process");
18
+ const PINENTRY_PROGRAMS = [
19
+ 'pinentry-mac',
20
+ 'pinentry',
21
+ 'pinentry-gnome3',
22
+ 'pinentry-qt',
23
+ ];
24
+ // Assuan protocol percent-encodes %, CR, and LF in D data lines.
25
+ // Without decoding, a passphrase containing `%` would be silently corrupted
26
+ // (e.g. `a%b` -> `a%25b`), deriving a wrong passphraseState and exposing a
27
+ // different — empty — hidden wallet.
28
+ function decodeAssuanData(encoded) {
29
+ return encoded.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
30
+ }
31
+ exports.decodeAssuanData = decodeAssuanData;
32
+ // Parse pinentry stdout into either a passphrase or a cancellation signal.
33
+ // Handles two edge cases beyond the basic `D <data>` shape:
34
+ // 1. Multi-line D responses — long passphrases past Assuan's ~1000-byte
35
+ // line limit get split across several `D` lines and must be concatenated
36
+ // *before* percent-decoding (a split inside `%XX` would otherwise corrupt
37
+ // the byte).
38
+ // 2. CRLF line endings — pinentry-mac uses LF, but pinentry-gnome3/qt may
39
+ // emit CRLF; splitting on `\r?\n` strips the trailing CR that would
40
+ // otherwise become a literal trailing byte in the passphrase.
41
+ function parsePinentryStdout(stdout) {
42
+ // Pinentry error code 83886179 is the canonical "user cancelled" signal —
43
+ // surfaces either as a non-zero exit or as an ERR line.
44
+ const cancelled = stdout.includes('ERR 83886179') || stdout.includes('Operation cancelled');
45
+ const dataChunks = stdout
46
+ .split(/\r?\n/)
47
+ .filter(l => l.startsWith('D '))
48
+ .map(l => l.slice(2));
49
+ if (dataChunks.length > 0) {
50
+ return { data: decodeAssuanData(dataChunks.join('')), cancelled };
51
+ }
52
+ return { cancelled };
53
+ }
54
+ exports.parsePinentryStdout = parsePinentryStdout;
55
+ function findPinentry() {
56
+ for (const prog of PINENTRY_PROGRAMS) {
57
+ try {
58
+ const result = (0, node_child_process_1.execFileSync)('which', [prog], {
59
+ encoding: 'utf-8',
60
+ timeout: 2000,
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ });
63
+ if (result.trim())
64
+ return prog;
65
+ }
66
+ catch {
67
+ // Program not found — try the next one.
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ exports.findPinentry = findPinentry;
73
+ // CLI-variant policy differs from app-monorepo: we fall back to on-device
74
+ // entry instead of rejecting, because `onekey-hw` is used by AI agents that
75
+ // can't recover from a thrown error mid-flow. The device screen is always
76
+ // a valid second path.
77
+ function promptPassphraseViaPinentry() {
78
+ return new Promise((resolve, reject) => {
79
+ const pinentryBin = findPinentry();
80
+ if (!pinentryBin) {
81
+ process.stderr.write('[onekey-hw] No pinentry found, falling back to on-device entry.\n');
82
+ resolve({ value: '', passphraseOnDevice: true });
83
+ return;
84
+ }
85
+ const commands = [
86
+ 'SETDESC OneKey Hardware Wallet',
87
+ 'SETPROMPT Enter passphrase',
88
+ 'GETPIN',
89
+ 'BYE',
90
+ ].join('\n');
91
+ const child = (0, node_child_process_1.execFile)(pinentryBin, [], { timeout: 120000, encoding: 'utf-8' }, (error, stdout) => {
92
+ const { data, cancelled } = parsePinentryStdout(stdout ?? '');
93
+ if (error) {
94
+ if (error.killed || cancelled) {
95
+ process.stderr.write('[onekey-hw] Passphrase entry cancelled, falling back to on-device.\n');
96
+ resolve({ value: '', passphraseOnDevice: true });
97
+ return;
98
+ }
99
+ reject(error);
100
+ return;
101
+ }
102
+ if (data !== undefined) {
103
+ resolve({ value: data, passphraseOnDevice: false });
104
+ return;
105
+ }
106
+ if (cancelled) {
107
+ process.stderr.write('[onekey-hw] Passphrase entry cancelled, falling back to on-device.\n');
108
+ resolve({ value: '', passphraseOnDevice: true });
109
+ return;
110
+ }
111
+ // Empty passphrase (OK pressed with no input) — on-device fallback.
112
+ resolve({ value: '', passphraseOnDevice: true });
113
+ });
114
+ child.stdin?.write(commands);
115
+ child.stdin?.end();
116
+ });
117
+ }
118
+ exports.promptPassphraseViaPinentry = promptPassphraseViaPinentry;
package/dist/sdk.d.ts CHANGED
@@ -2,14 +2,24 @@
2
2
  * SDK Factory — creates and initializes the hardware SDK instance
3
3
  * for CLI usage with the appropriate transport.
4
4
  *
5
- * CRITICAL: Must register UI event handlers for PIN, Passphrase, and Button
6
- * confirmation. Without these, the SDK will hang waiting for responses.
7
- *
8
- * Reference: packages/core/src/core/index.ts (event registration pattern)
5
+ * Passphrase flow aligns with app-monorepo CLI:
6
+ * - Standard wallet: --use-empty-passphrase, auto-respond
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
9
10
  */
11
+ import HardwareSDK from '@onekeyfe/hd-common-connect-sdk';
10
12
  export interface SDKOptions {
11
13
  connectId?: string;
12
14
  passphraseState?: string;
13
15
  useEmptyPassphrase?: boolean;
14
16
  }
15
- export declare function createSDK(opts: SDKOptions): Promise<import("@onekeyfe/hd-core").CoreApi>;
17
+ export declare function createSDK(opts: SDKOptions): Promise<typeof HardwareSDK>;
18
+ /**
19
+ * Release the SDK and clear the singleton. Must be called before the CLI
20
+ * process exits — the USB transport holds open handles (event listeners,
21
+ * polling timers) that otherwise keep Node.js alive for ~26s.
22
+ *
23
+ * Safe to call multiple times (no-op after the first successful call).
24
+ */
25
+ export declare function disposeSDK(): Promise<void>;
package/dist/sdk.js CHANGED
@@ -1,189 +1,233 @@
1
- 'use strict';
2
-
1
+ "use strict";
3
2
  /**
4
3
  * SDK Factory — creates and initializes the hardware SDK instance
5
4
  * for CLI usage with the appropriate transport.
6
5
  *
7
- * CRITICAL: Must register UI event handlers for PIN, Passphrase, and Button
8
- * confirmation. Without these, the SDK will hang waiting for responses.
9
- *
10
- * Reference: packages/core/src/core/index.ts (event registration pattern)
6
+ * Passphrase flow aligns with app-monorepo CLI:
7
+ * - Standard wallet: --use-empty-passphrase, auto-respond
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
11
11
  */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
12
38
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.createSDK = void 0;
14
- const tslib_1 = require("tslib");
15
- // @ts-ignore - hd-common-connect-sdk may not have type declarations
16
- const hd_common_connect_sdk_1 = tslib_1.__importDefault(require("@onekeyfe/hd-common-connect-sdk"));
39
+ exports.disposeSDK = exports.createSDK = void 0;
40
+ const readline = __importStar(require("node:readline"));
41
+ const hd_common_connect_sdk_1 = __importDefault(require("@onekeyfe/hd-common-connect-sdk"));
17
42
  const hd_core_1 = require("@onekeyfe/hd-core");
18
- const hd_transport_usb_1 = require("@onekeyfe/hd-transport-usb");
19
- const readline = tslib_1.__importStar(require("readline"));
43
+ const pinentry_1 = require("./pinentry");
44
+ /**
45
+ * Current per-invocation CLI options. Event handlers read from this object
46
+ * so that invoking createSDK() with different opts never results in stale
47
+ * closure captures — the SDK is a singleton, but opts can change per call.
48
+ */
49
+ let currentOpts = {};
20
50
  /**
21
- * Prompt user for input in the terminal (hidden for PIN).
22
- * Falls back to empty string in non-TTY (piped) mode.
51
+ * Promise-singleton that resolves to the initialised SDK. Set on first call,
52
+ * reused by all subsequent (and concurrent) callers.
53
+ *
54
+ * Using a Promise instead of a boolean flag eliminates the race where two
55
+ * concurrent createSDK() calls could both pass the "initialised?" check
56
+ * before the flag was set, then both run init() — each call replaces the
57
+ * internal Core/Connector at the hd-core layer and leaks USB handles.
58
+ *
59
+ * Mirrors app-monorepo's apps/cli/src/commands/device/hardware-sdk.ts
60
+ * (sdkReadyPromise). Cleared by disposeSDK() so REPL/test harnesses can
61
+ * re-init cleanly after an explicit tear-down.
23
62
  */
24
- function promptUser(question, hidden = false) {
63
+ let sdkReadyPromise = null;
64
+ // ---------------------------------------------------------------------------
65
+ // Interactive prompts
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Prompt user to select wallet type (aligns with app-monorepo flow):
69
+ * 1. Standard wallet (no passphrase)
70
+ * 2. Hidden wallet — enter passphrase via pinentry (secure OS dialog)
71
+ * 3. Hidden wallet — enter passphrase on device screen
72
+ */
73
+ function resolvePassphraseByChoice(choice) {
74
+ if (choice === '1')
75
+ return Promise.resolve({ value: '', passphraseOnDevice: false });
76
+ if (choice === '2')
77
+ return (0, pinentry_1.promptPassphraseViaPinentry)();
78
+ return Promise.resolve({ value: '', passphraseOnDevice: true });
79
+ }
80
+ function promptPassphraseMode() {
25
81
  if (!process.stdin.isTTY) {
26
- // Non-interactive mode: return empty (agent should handle via uiResponse)
27
- return Promise.resolve('');
82
+ return Promise.resolve({ value: '', passphraseOnDevice: true });
28
83
  }
29
84
  return new Promise(resolve => {
30
85
  const rl = readline.createInterface({
31
86
  input: process.stdin,
32
- output: process.stderr, // Use stderr so JSON stdout stays clean
87
+ output: process.stderr,
88
+ terminal: true,
33
89
  });
34
- if (hidden) {
35
- // Mute output for PIN entry
36
- process.stderr.write(question);
37
- const { stdin } = process;
38
- const wasRaw = stdin.isRaw;
39
- if (stdin.setRawMode)
40
- stdin.setRawMode(true);
41
- let input = '';
42
- const onData = (char) => {
43
- const c = char.toString('utf8');
44
- if (c === '\n' || c === '\r' || c === '\u0004') {
45
- if (stdin.setRawMode)
46
- stdin.setRawMode(wasRaw ?? false);
47
- stdin.removeListener('data', onData);
48
- process.stderr.write('\n');
90
+ const prompt = () => {
91
+ process.stderr.write([
92
+ '[onekey-hw] Select wallet type:',
93
+ ' 1. Standard wallet (no passphrase)',
94
+ ' 2. Hidden wallet — enter passphrase on this computer (pinentry)',
95
+ ' 3. Hidden wallet — enter passphrase on device screen',
96
+ '',
97
+ ].join('\n'));
98
+ rl.question('Enter selection [1/2/3]: ', answer => {
99
+ const n = answer.trim();
100
+ if (n === '1' || n === '2' || n === '3') {
49
101
  rl.close();
50
- resolve(input);
51
- }
52
- else if (c === '\u0003') {
53
- // Ctrl+C
54
- process.exit(1);
55
- }
56
- else if (c === '\u007F' || c === '\b') {
57
- // Backspace
58
- input = input.slice(0, -1);
102
+ resolvePassphraseByChoice(n).then(resolve);
103
+ return;
59
104
  }
60
- else {
61
- input += c;
62
- process.stderr.write('*');
63
- }
64
- };
65
- stdin.on('data', onData);
66
- }
67
- else {
68
- rl.question(question, answer => {
69
- rl.close();
70
- resolve(answer);
105
+ process.stderr.write('Invalid selection. Enter 1, 2, or 3.\n');
106
+ prompt();
71
107
  });
72
- }
108
+ };
109
+ prompt();
73
110
  });
74
111
  }
75
- /**
76
- * Register UI event handlers for interactive device operations.
77
- *
78
- * The SDK emits events when the device needs user interaction:
79
- * - PIN entry (entered on device screen for Touch/Pro, or via matrix for Classic)
80
- * - Passphrase input (for hidden wallets)
81
- * - Button confirmation (user must physically press on device)
82
- *
83
- * Reference: packages/core/src/core/index.ts lines 315-330, 1021-1098
84
- */
85
- function registerEventHandlers(sdk, opts) {
112
+ // ---------------------------------------------------------------------------
113
+ // Event handlers
114
+ // ---------------------------------------------------------------------------
115
+ function registerEventHandlers(sdk) {
86
116
  sdk.on(hd_core_1.UI_EVENT, (message) => {
87
- // PIN Request
88
- // For Touch/Pro devices, PIN is entered on-device (device screen shows numpad).
89
- // For Classic devices, PIN uses a matrix mapping.
90
- // In CLI context, we auto-acknowledge since PIN entry happens on-device.
117
+ // PIN Request — always on-device for CLI security (no terminal echo).
118
+ // The sentinel '@@ONEKEY_INPUT_PIN_IN_DEVICE' makes DeviceCommands send
119
+ // `BixinPinInputOnDevice` so the device switches to on-device PIN entry.
120
+ // Sending an empty string would be treated as a wrong (empty) PIN and
121
+ // would consume a PIN retry on Classic/1S/Mini/Pure.
122
+ // Touch/Pro never emit PinMatrixRequest — they handle PIN on touchscreen
123
+ // before this code path runs, so the sentinel is harmless there.
91
124
  if (message.type === hd_core_1.UI_REQUEST.REQUEST_PIN) {
92
- const pinType = message.payload?.type;
93
- if (pinType === 'ButtonRequest_PinEntry' || pinType === 'ButtonRequest_AttachPin') {
94
- // PIN is entered directly on device screen (Touch/Pro)
95
- process.stderr.write('[onekey-hw] Please enter PIN on your device screen...\n');
96
- // No uiResponse needed — device handles PIN input internally
97
- }
98
- else {
99
- // Classic devices: PIN entry via matrix
100
- // In CLI mode, prompt user or let agent handle
101
- process.stderr.write('[onekey-hw] PIN required. Please enter PIN on your device.\n');
102
- promptUser('PIN (on-device numpad mapping): ', true).then(pin => {
103
- sdk.uiResponse({
104
- type: hd_core_1.UI_RESPONSE.RECEIVE_PIN,
105
- payload: pin,
106
- });
107
- });
108
- }
125
+ process.stderr.write('[onekey-hw] Please enter PIN on your device screen...\n');
126
+ sdk.uiResponse({
127
+ type: hd_core_1.UI_RESPONSE.RECEIVE_PIN,
128
+ payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE',
129
+ });
109
130
  }
110
131
  // Passphrase Request
111
- // User must provide passphrase for hidden wallet access.
112
- // Passphrase can be entered on-device (Touch/Pro) or via host.
113
132
  if (message.type === hd_core_1.UI_REQUEST.REQUEST_PASSPHRASE) {
114
- if (opts.useEmptyPassphrase) {
115
- // Standard wallet (no passphrase)
133
+ // 1. Explicit --use-empty-passphrase: auto-respond
134
+ if (currentOpts.useEmptyPassphrase) {
135
+ sdk.uiResponse({
136
+ type: hd_core_1.UI_RESPONSE.RECEIVE_PASSPHRASE,
137
+ payload: { value: '', passphraseOnDevice: false, save: false },
138
+ });
139
+ return;
140
+ }
141
+ // 2. Interactive: 1/2/3 selection
142
+ promptPassphraseMode()
143
+ .then(result => {
116
144
  sdk.uiResponse({
117
145
  type: hd_core_1.UI_RESPONSE.RECEIVE_PASSPHRASE,
118
146
  payload: {
119
- value: '',
120
- passphraseOnDevice: false,
147
+ value: result.value,
148
+ passphraseOnDevice: result.passphraseOnDevice,
121
149
  save: false,
122
150
  },
123
151
  });
124
- }
125
- else {
126
- process.stderr.write('[onekey-hw] Passphrase required for hidden wallet.\n');
127
- promptUser('Enter passphrase (or press Enter for on-device entry): ').then(passphrase => {
128
- if (passphrase === '') {
129
- // Enter on device
130
- sdk.uiResponse({
131
- type: hd_core_1.UI_RESPONSE.RECEIVE_PASSPHRASE,
132
- payload: {
133
- value: '',
134
- passphraseOnDevice: true,
135
- save: false,
136
- },
137
- });
138
- }
139
- else {
140
- sdk.uiResponse({
141
- type: hd_core_1.UI_RESPONSE.RECEIVE_PASSPHRASE,
142
- payload: {
143
- value: passphrase,
144
- passphraseOnDevice: false,
145
- save: false,
146
- },
147
- });
148
- }
152
+ })
153
+ .catch(() => {
154
+ process.stderr.write('[onekey-hw] Passphrase prompt failed, falling back to on-device.\n');
155
+ sdk.uiResponse({
156
+ type: hd_core_1.UI_RESPONSE.RECEIVE_PASSPHRASE,
157
+ payload: { value: '', passphraseOnDevice: true, save: false },
149
158
  });
150
- }
159
+ });
151
160
  }
152
161
  // Passphrase On Device
153
162
  if (message.type === hd_core_1.UI_REQUEST.REQUEST_PASSPHRASE_ON_DEVICE) {
154
163
  process.stderr.write('[onekey-hw] Please enter passphrase on your device screen...\n');
155
164
  }
156
165
  // Button Confirmation
157
- // User must physically press confirm/reject on the device.
158
166
  if (message.type === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
159
167
  process.stderr.write('[onekey-hw] Please confirm the action on your device...\n');
160
168
  }
161
169
  });
162
- // Device connection events — only show when device has a known name
163
170
  sdk.on(hd_core_1.DEVICE.CONNECT, (device) => {
164
171
  const name = device?.label || device?.name;
165
- if (name) {
172
+ if (name)
166
173
  process.stderr.write(`[onekey-hw] Device connected: ${name}\n`);
167
- }
168
174
  });
169
175
  sdk.on(hd_core_1.DEVICE.DISCONNECT, (device) => {
170
176
  const name = device?.label || device?.name;
171
- if (name) {
177
+ if (name)
172
178
  process.stderr.write(`[onekey-hw] Device disconnected: ${name}\n`);
173
- }
174
179
  });
175
180
  }
176
- async function createSDK(opts) {
181
+ // ---------------------------------------------------------------------------
182
+ // SDK Factory
183
+ // ---------------------------------------------------------------------------
184
+ async function initSDK() {
177
185
  const settings = {
178
186
  debug: false,
179
187
  fetchConfig: true,
188
+ env: 'node-usb',
180
189
  };
181
- // Direct USB via libusb — works on macOS, Linux, Windows
182
- settings.env = 'lowlevel';
183
- const plugin = hd_transport_usb_1.UsbPlugin;
184
- await hd_common_connect_sdk_1.default.init(settings, undefined, plugin);
185
- // Register event handlers AFTER init
186
- registerEventHandlers(hd_common_connect_sdk_1.default, opts);
190
+ await hd_common_connect_sdk_1.default.init(settings);
191
+ // Defensive: strip any stale listeners (e.g. left over from a previous
192
+ // dispose/init cycle in a long-running process) before wiring ours.
193
+ // Mirrors app-monorepo's cleanupHardwareSDKInstance() which removes
194
+ // listeners for ALL events at once.
195
+ // @ts-expect-error CoreApi types require an event name; passing none
196
+ // removes listeners for ALL events (Node.js EventEmitter semantics).
197
+ hd_common_connect_sdk_1.default.removeAllListeners();
198
+ registerEventHandlers(hd_common_connect_sdk_1.default);
187
199
  return hd_common_connect_sdk_1.default;
188
200
  }
201
+ async function createSDK(opts) {
202
+ // Always refresh opts — event handlers read from the module-level ref,
203
+ // so subsequent createSDK() calls pick up new opts without re-registering.
204
+ currentOpts = opts;
205
+ if (!sdkReadyPromise) {
206
+ sdkReadyPromise = initSDK();
207
+ }
208
+ return sdkReadyPromise;
209
+ }
189
210
  exports.createSDK = createSDK;
211
+ /**
212
+ * Release the SDK and clear the singleton. Must be called before the CLI
213
+ * process exits — the USB transport holds open handles (event listeners,
214
+ * polling timers) that otherwise keep Node.js alive for ~26s.
215
+ *
216
+ * Safe to call multiple times (no-op after the first successful call).
217
+ */
218
+ async function disposeSDK() {
219
+ if (!sdkReadyPromise)
220
+ return;
221
+ try {
222
+ const sdk = await sdkReadyPromise;
223
+ sdk.dispose();
224
+ }
225
+ catch {
226
+ // ignore errors during cleanup
227
+ }
228
+ finally {
229
+ sdkReadyPromise = null;
230
+ currentOpts = {};
231
+ }
232
+ }
233
+ exports.disposeSDK = disposeSDK;