/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; import { DebugLogger } from '@vybestack/llxprt-code-telemetry'; import { Colors } from '../colors.js'; const logger = new DebugLogger('llxprt:ui:secure-key-input'); export interface SecureKeyInputProps { onSubmit: (value: string) => void; onCancel: () => void; providerName: string; } const isEioError = (err: unknown): boolean => { if (err == null || typeof err !== 'object') { return false; } const nodeError = err as NodeJS.ErrnoException; return ( nodeError.code === 'EIO' || nodeError.errno === -5 || (typeof nodeError.message === 'string' && nodeError.message.includes('EIO')) ); }; const KeyInputInstructions: React.FC = () => ( Press Enter to submit Press Ctrl+V to toggle visibility Press Escape to cancel ); function updateKeyValue( char: string, key: { name?: string; ctrl?: boolean }, setKeyValue: React.Dispatch>, ): boolean { if (key.name === 'backspace') { setKeyValue((prev) => prev.slice(0, -1)); return true; } if (char.length > 0 && char.length === 1 && key.ctrl !== true) { setKeyValue((prev) => prev + char); return true; } return false; } /** * Component for secure API key input with masking */ export const SecureKeyInput: React.FC = ({ onSubmit, onCancel, providerName, }) => { const [keyValue, setKeyValue] = useState(''); const [showKey, setShowKey] = useState(false); const handleKeyPress = useCallback( (char: string, key: { name?: string; ctrl?: boolean }) => { if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) { onCancel(); return; } if (key.name === 'return') { onSubmit(keyValue); return; } if (key.ctrl === true && key.name === 'v') { setShowKey((prev) => !prev); return; } updateKeyValue(char, key, setKeyValue); }, [keyValue, onSubmit, onCancel], ); useEffect(() => { let rawModeEnabled = false; const handleInput = (chunk: Buffer, key: unknown) => { const keyInfo = key as { name?: string; ctrl?: boolean }; const char = chunk.toString(); handleKeyPress(char, keyInfo); }; const handleError = (err: Error) => { if (!isEioError(err)) { logger.error('Stdin error in SecureKeyInput:', err); } }; process.stdin.on('data', handleInput); process.stdin.on('error', handleError); if (process.stdin.isTTY) { try { process.stdin.setRawMode(true); rawModeEnabled = true; } catch (error) { logger.debug('Failed to enable raw mode for SecureKeyInput', error); } } process.stdin.resume(); return () => { process.stdin.off('data', handleInput); process.stdin.off('error', handleError); if (rawModeEnabled && process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch { // Issue #1020: Ignore cleanup errors } } process.stdin.pause(); }; }, [handleKeyPress]); const maskedValue = showKey ? keyValue : '•'.repeat(keyValue.length); return ( Enter API key for {providerName}: {maskedValue} {keyValue.length > 0 && ( ({keyValue.length} characters) )} ); };