Skip to main content
Glama
credentials.ts17.7 kB
/** * WP Navigator Credentials Command * * Manage credentials stored in OS keychain. * Supports macOS Keychain and Linux Secret Service. * * Usage: * wpnav credentials store --site example.com # Store credential * wpnav credentials show --site example.com # Show credential * wpnav credentials clear --site example.com # Remove credential * wpnav credentials list # List all credentials * * @package WP_Navigator_MCP * @since 2.7.0 */ import { getKeychainProvider, isKeychainAvailable, getKeychainProviderName, createKeychainReference, KEYCHAIN_SERVICE, } from '../credentials/index.js'; import { inputPrompt, confirmPrompt } from '../tui/prompts.js'; import { success, error as errorMessage, warning, info, newline, box, keyValue, list, colorize, symbols, } from '../tui/components.js'; // ============================================================================= // Types // ============================================================================= export interface CredentialsOptions { /** Output JSON instead of TUI */ json?: boolean; /** Site domain for store/show/clear operations */ site?: string; /** Skip confirmation prompts */ yes?: boolean; /** Reveal password in clear text (default: masked) */ reveal?: boolean; } export type CredentialsAction = 'store' | 'show' | 'clear' | 'list' | 'status'; // ============================================================================= // JSON Output // ============================================================================= interface JsonResult { success: boolean; command: string; data?: unknown; error?: { code: string; message: string; }; } function outputJSON(result: JsonResult): void { // Safe: passwords in JsonResult are always masked before being passed here (see maskPassword usage) // lgtm[js/clear-text-logging] console.log(JSON.stringify(result, null, 2)); } // ============================================================================= // Credential Input // ============================================================================= /** * Prompt for password input via stdin (for piped input in JSON mode) */ async function readPasswordFromStdin(): Promise<string> { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let resolved = false; // Set a timeout in case stdin isn't available const timeout = setTimeout(() => { if (!resolved) { resolved = true; resolve(''); } }, 100); process.stdin.on('data', (chunk) => { chunks.push(chunk); }); process.stdin.on('end', () => { if (!resolved) { resolved = true; clearTimeout(timeout); resolve(Buffer.concat(chunks).toString('utf8').trim()); } }); process.stdin.on('error', (err) => { if (!resolved) { resolved = true; clearTimeout(timeout); reject(err); } }); // If stdin is a TTY, it won't have piped data if (process.stdin.isTTY) { resolved = true; clearTimeout(timeout); resolve(''); } }); } /** * Mask a password for display */ function maskPassword(password: string, showFirst = 4): string { if (password.length <= showFirst) { return '*'.repeat(password.length); } return password.slice(0, showFirst) + '*'.repeat(Math.min(password.length - showFirst, 16)); } // ============================================================================= // Store Command // ============================================================================= async function handleStore(options: CredentialsOptions): Promise<number> { const provider = getKeychainProvider(); const isJson = options.json === true; // Check keychain availability if (!provider.available) { if (isJson) { outputJSON({ success: false, command: 'credentials store', error: { code: 'KEYCHAIN_UNAVAILABLE', message: `Keychain not available: ${provider.name}`, }, }); } else { errorMessage(`Keychain not available: ${provider.name}`); newline(); info('Store credentials in .wpnav.env file instead.'); } return 1; } let site = options.site; let password: string; // Get site domain if (!site) { if (isJson) { outputJSON({ success: false, command: 'credentials store', error: { code: 'MISSING_SITE', message: 'Site is required. Use --site <domain>', }, }); return 1; } site = await inputPrompt({ message: 'Site domain (e.g., example.com)', validate: (v) => (v.length >= 3 ? null : 'Domain must be at least 3 characters'), }); } // Normalize site - remove protocol and trailing slash site = site.replace(/^https?:\/\//, '').replace(/\/+$/, ''); // Get password if (isJson) { // In JSON mode, require --yes and read password from stdin if (!options.yes) { outputJSON({ success: false, command: 'credentials store', error: { code: 'CONFIRMATION_REQUIRED', message: 'JSON mode requires --yes flag. Pass password via stdin.', }, }); return 1; } password = await readPasswordFromStdin(); if (!password) { outputJSON({ success: false, command: 'credentials store', error: { code: 'MISSING_PASSWORD', message: 'Password required via stdin. Example: echo "password" | wpnav credentials store --site example.com --json --yes', }, }); return 1; } } else { // Interactive TUI mode - use readline for password const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stderr, }); password = await new Promise<string>((resolve) => { process.stderr.write('Application Password: '); rl.question('', (answer) => { rl.close(); resolve(answer.trim()); }); }); if (!password) { errorMessage('Password is required.'); return 1; } } // Store the credential try { await provider.store({ service: KEYCHAIN_SERVICE, account: site, password, }); if (isJson) { outputJSON({ success: true, command: 'credentials store', data: { account: site, keychain_reference: createKeychainReference(site), }, }); } else { newline(); success(`Credential stored for ${site}`); newline(); info('Use in wpnav.config.json:'); console.error(` "password": "${createKeychainReference(site)}"`); } return 0; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); if (isJson) { outputJSON({ success: false, command: 'credentials store', error: { code: 'STORE_FAILED', message: errMsg }, }); } else { errorMessage(`Failed to store credential: ${errMsg}`); } return 1; } } // ============================================================================= // Show Command // ============================================================================= async function handleShow(options: CredentialsOptions): Promise<number> { const provider = getKeychainProvider(); const isJson = options.json === true; const site = options.site; const revealPassword = options.reveal === true; // Check keychain availability if (!provider.available) { if (isJson) { outputJSON({ success: false, command: 'credentials show', error: { code: 'KEYCHAIN_UNAVAILABLE', message: `Keychain not available: ${provider.name}`, }, }); } else { errorMessage(`Keychain not available: ${provider.name}`); } return 1; } // Require site if (!site) { if (isJson) { outputJSON({ success: false, command: 'credentials show', error: { code: 'MISSING_SITE', message: 'Site required. Use --site <domain>' }, }); } else { errorMessage('Site required. Use --site <domain>'); } return 1; } // Normalize site const normalizedSite = site.replace(/^https?:\/\//, '').replace(/\/+$/, ''); // Retrieve credential try { const password = await provider.retrieve(normalizedSite); if (!password) { if (isJson) { outputJSON({ success: false, command: 'credentials show', error: { code: 'NOT_FOUND', message: `No credential found for ${normalizedSite}` }, }); } else { warning(`No credential found for ${normalizedSite}`); } return 1; } if (isJson) { // JSON mode: mask password unless --reveal is used // This prevents accidental exposure in logs/scripts const outputPassword = revealPassword ? password : maskPassword(password); outputJSON({ success: true, command: 'credentials show', data: { account: normalizedSite, password: outputPassword, masked: !revealPassword, keychain_reference: createKeychainReference(normalizedSite), }, }); } else { newline(); keyValue('Site', normalizedSite); // TUI mode: mask password unless --reveal is used // This prevents shoulder-surfing and accidental log exposure const displayPassword = revealPassword ? password : maskPassword(password); keyValue('Password', displayPassword); keyValue('Reference', createKeychainReference(normalizedSite)); newline(); if (revealPassword) { warning('Handle this password carefully - do not share or log it.'); } else { info('Use --reveal to show the full password.'); } } return 0; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); if (isJson) { outputJSON({ success: false, command: 'credentials show', error: { code: 'RETRIEVE_FAILED', message: errMsg }, }); } else { errorMessage(`Failed to retrieve credential: ${errMsg}`); } return 1; } } // ============================================================================= // Clear Command // ============================================================================= async function handleClear(options: CredentialsOptions): Promise<number> { const provider = getKeychainProvider(); const isJson = options.json === true; const site = options.site; const skipConfirm = options.yes === true; // Check keychain availability if (!provider.available) { if (isJson) { outputJSON({ success: false, command: 'credentials clear', error: { code: 'KEYCHAIN_UNAVAILABLE', message: `Keychain not available: ${provider.name}`, }, }); } else { errorMessage(`Keychain not available: ${provider.name}`); } return 1; } // Require site if (!site) { if (isJson) { outputJSON({ success: false, command: 'credentials clear', error: { code: 'MISSING_SITE', message: 'Site required. Use --site <domain>' }, }); } else { errorMessage('Site required. Use --site <domain>'); } return 1; } // Normalize site const normalizedSite = site.replace(/^https?:\/\//, '').replace(/\/+$/, ''); // Confirm deletion in TUI mode if (!isJson && !skipConfirm) { const confirmed = await confirmPrompt({ message: `Delete credential for ${normalizedSite}?`, defaultValue: false, }); if (!confirmed) { info('Cancelled. No credential was deleted.'); return 0; } } // Delete credential try { const deleted = await provider.delete(normalizedSite); if (isJson) { outputJSON({ success: true, command: 'credentials clear', data: { account: normalizedSite, deleted, }, }); } else { if (deleted) { success(`Credential deleted for ${normalizedSite}`); } else { warning(`No credential found for ${normalizedSite}`); } } return 0; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); if (isJson) { outputJSON({ success: false, command: 'credentials clear', error: { code: 'DELETE_FAILED', message: errMsg }, }); } else { errorMessage(`Failed to delete credential: ${errMsg}`); } return 1; } } // ============================================================================= // List Command // ============================================================================= async function handleList(options: CredentialsOptions): Promise<number> { const provider = getKeychainProvider(); const isJson = options.json === true; // Check keychain availability if (!provider.available) { if (isJson) { outputJSON({ success: false, command: 'credentials list', error: { code: 'KEYCHAIN_UNAVAILABLE', message: `Keychain not available: ${provider.name}`, }, }); } else { errorMessage(`Keychain not available: ${provider.name}`); } return 1; } // List credentials try { const accounts = await provider.list(); if (isJson) { outputJSON({ success: true, command: 'credentials list', data: { provider: provider.name, accounts, count: accounts.length, }, }); } else { newline(); box(`Stored Credentials (${provider.name})`); newline(); if (accounts.length === 0) { info('No credentials stored.'); newline(); info('Use `wpnav credentials store --site <domain>` to add credentials.'); } else { for (const account of accounts) { console.error(` ${colorize(symbols.success, 'green')} ${account}`); console.error(` ${colorize(createKeychainReference(account), 'dim')}`); } newline(); info(`${accounts.length} credential${accounts.length > 1 ? 's' : ''} stored.`); } } return 0; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); if (isJson) { outputJSON({ success: false, command: 'credentials list', error: { code: 'LIST_FAILED', message: errMsg }, }); } else { errorMessage(`Failed to list credentials: ${errMsg}`); } return 1; } } // ============================================================================= // Status Command // ============================================================================= async function handleStatus(options: CredentialsOptions): Promise<number> { const provider = getKeychainProvider(); const isJson = options.json === true; if (isJson) { outputJSON({ success: true, command: 'credentials status', data: { provider: provider.name, available: provider.available, platform: process.platform, }, }); } else { newline(); box('Keychain Status'); newline(); keyValue('Platform', process.platform); keyValue('Provider', provider.name); keyValue('Available', provider.available ? colorize('Yes', 'green') : colorize('No', 'red')); if (!provider.available) { newline(); if (process.platform === 'linux') { info('Install libsecret-tools for keychain support:'); console.error(' apt install libsecret-tools # Debian/Ubuntu'); console.error(' dnf install libsecret # Fedora'); } else if (process.platform === 'win32') { info('Windows Credential Manager is not yet supported.'); info('Store credentials in .wpnav.env file instead.'); } } } return 0; } // ============================================================================= // Main Handler // ============================================================================= /** * Handle the credentials command * * @param action - The subcommand (store, show, clear, list, status) * @param options - Command options * @returns Exit code: 0 for success, 1 for errors */ export async function handleCredentials( action: CredentialsAction, options: CredentialsOptions = {} ): Promise<number> { switch (action) { case 'store': return handleStore(options); case 'show': return handleShow(options); case 'clear': return handleClear(options); case 'list': return handleList(options); case 'status': return handleStatus(options); default: if (options.json) { outputJSON({ success: false, command: 'credentials', error: { code: 'INVALID_ACTION', message: `Invalid action: ${action}. Use: store, show, clear, list, or status`, }, }); } else { errorMessage(`Invalid action: ${action}`); newline(); info('Available actions:'); list([ 'store --site <domain> Store credential in keychain', 'show --site <domain> Show stored credential', 'clear --site <domain> Remove credential from keychain', 'list List all stored credentials', 'status Show keychain status', ]); } return 1; } } export default handleCredentials;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server