Skip to main content
Glama
index.ts10.4 kB
/** * Credential Management - Public API * * Factory and utilities for OS keychain credential storage. * Automatically selects the appropriate provider for the current platform. * * Usage: * import { getKeychainProvider, resolvePassword } from './cli/credentials/index.js'; * * // Store a credential * const provider = getKeychainProvider(); * if (provider.available) { * await provider.store({ service: 'wpnav', account: 'example.com', password: 'xxx' }); * } * * // Resolve a password (handles keychain references) * const result = await resolvePassword('keychain:wpnav:example.com'); * if (result.password) { * // Use the password * } * * @package WP_Navigator_MCP * @since 2.7.0 */ import { execFileSync } from 'child_process'; import type { KeychainProvider, CredentialResult, CredentialSource } from './types.js'; import { KEYCHAIN_PREFIX, KEYCHAIN_SERVICE } from './types.js'; import { MacOSKeychainProvider } from './macos.js'; import { LinuxSecretServiceProvider } from './linux.js'; import { WindowsCredentialProvider } from './windows.js'; import { FallbackProvider } from './fallback.js'; // Re-export types for convenience export * from './types.js'; export { BaseKeychainProvider } from './keychain.js'; // Singleton provider instance let _provider: KeychainProvider | null = null; /** * Get the appropriate keychain provider for the current platform * * Selection order: * 1. macOS Keychain (if on macOS) * 2. Linux Secret Service (if on Linux with secret-tool) * 3. Windows Credential Manager (if on Windows with cmdkey) * 4. Fallback (always unavailable) * * The provider is cached for the lifetime of the process. */ export function getKeychainProvider(): KeychainProvider { if (_provider) { return _provider; } // Try macOS first const macOS = new MacOSKeychainProvider(); if (macOS.available) { _provider = macOS; return _provider; } // Try Linux const linux = new LinuxSecretServiceProvider(); if (linux.available) { _provider = linux; return _provider; } // Try Windows const windows = new WindowsCredentialProvider(); if (windows.available) { _provider = windows; return _provider; } // Fallback - always available but always fails _provider = new FallbackProvider(); return _provider; } /** * Check if keychain storage is available on this platform */ export function isKeychainAvailable(): boolean { return getKeychainProvider().available; } /** * Get the name of the current keychain provider */ export function getKeychainProviderName(): string { return getKeychainProvider().name; } /** * Parse a keychain reference from a config password field * * Format: "keychain:wpnav:example.com" * * @param value - The password field value * @returns Parsed reference or null if not a keychain reference */ export function parseKeychainReference(value: string): { account: string } | null { if (!value || typeof value !== 'string') { return null; } if (!value.startsWith(KEYCHAIN_PREFIX)) { return null; } // Parse: keychain:wpnav:example.com const rest = value.slice(KEYCHAIN_PREFIX.length); const parts = rest.split(':'); if (parts.length !== 2) { return null; } const [service, account] = parts; // Validate service name if (service !== KEYCHAIN_SERVICE) { return null; } // Validate account exists if (!account || account.length < 3) { return null; } return { account }; } /** * Create a keychain reference string for a site * * @param account - Site domain (e.g., 'example.com') * @returns Keychain reference string (e.g., 'keychain:wpnav:example.com') */ export function createKeychainReference(account: string): string { return `${KEYCHAIN_PREFIX}${KEYCHAIN_SERVICE}:${account}`; } /** * Resolve a password value - handles both literal values and keychain references * * This is the main entry point for config loading. * * @param value - Password field value (literal or keychain reference) * @returns Resolved credential with source information */ export async function resolvePassword(value: string): Promise<CredentialResult> { // Check for keychain reference const ref = parseKeychainReference(value); if (!ref) { // Not a keychain reference, return as literal value return { source: 'config' as CredentialSource, password: value, }; } // It's a keychain reference - try to retrieve const provider = getKeychainProvider(); if (!provider.available) { return { source: 'none' as CredentialSource, password: null, error: `Keychain not available (${provider.name}). Install keychain tools or use plaintext password.`, }; } try { const password = await provider.retrieve(ref.account); if (!password) { return { source: 'none' as CredentialSource, password: null, error: `Credential not found in keychain for account: ${ref.account}`, }; } return { source: 'keychain' as CredentialSource, password, }; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); return { source: 'none' as CredentialSource, password: null, error: `Failed to retrieve from keychain: ${errMsg}`, }; } } /** * Check if a password value is a keychain reference * * @param value - Password field value * @returns true if it's a keychain reference */ export function isKeychainReference(value: string): boolean { return parseKeychainReference(value) !== null; } /** * Resolve a password value synchronously - for config loading * * Uses synchronous system calls (execFileSync) under the hood. * Falls back gracefully if keychain is unavailable. * * @param value - Password field value (literal or keychain reference) * @returns Resolved password or throws an error */ export function resolvePasswordSync(value: string): string { // Check for keychain reference const ref = parseKeychainReference(value); if (!ref) { // Not a keychain reference, return as-is return value; } // It's a keychain reference - try to retrieve const provider = getKeychainProvider(); if (!provider.available) { throw new Error( `Keychain reference found but keychain not available (${provider.name}). ` + `Install keychain tools or use plaintext password.` ); } // Use execFileSync directly for synchronous operation (imported at top of file) if (process.platform === 'darwin') { // macOS Keychain try { const result = execFileSync( 'security', ['find-generic-password', '-a', KEYCHAIN_SERVICE, '-s', ref.account, '-w'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, } ); const password = (result as string).trim(); if (!password) { throw new Error(`Credential not found in keychain for account: ${ref.account}`); } return password; } catch (err) { if (err instanceof Error && err.message.includes('Credential not found')) { throw err; } throw new Error(`Failed to retrieve credential from keychain: ${ref.account}`); } } else if (process.platform === 'linux') { // Linux Secret Service try { const result = execFileSync( 'secret-tool', ['lookup', 'service', KEYCHAIN_SERVICE, 'account', ref.account], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, } ); const password = (result as string).trim(); if (!password) { throw new Error(`Credential not found in keychain for account: ${ref.account}`); } return password; } catch { throw new Error(`Failed to retrieve credential from keychain: ${ref.account}`); } } else if (process.platform === 'win32') { // Windows Credential Manager via PowerShell P/Invoke const target = 'wpnav-' + ref.account; const script = ` Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class WpnavCredManager { [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CredReadW(string target, int type, int flags, out IntPtr credential); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool CredFree(IntPtr credential); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct CREDENTIAL { public int Flags; public int Type; public string TargetName; public string Comment; public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist; public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName; } public static string GetPassword(string target) { IntPtr credPtr; if (CredReadW(target, 1, 0, out credPtr)) { try { CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL)); if (cred.CredentialBlobSize > 0) { return Marshal.PtrToStringUni(cred.CredentialBlob, cred.CredentialBlobSize / 2); } } finally { CredFree(credPtr); } } return null; } } '@ $result = [WpnavCredManager]::GetPassword('${target}') if ($result) { Write-Output $result } `; try { const result = execFileSync( 'powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000, windowsHide: true, } ); const password = (result as string).trim(); if (!password) { throw new Error(`Credential not found in keychain for account: ${ref.account}`); } return password; } catch (err) { if (err instanceof Error && err.message.includes('Credential not found')) { throw err; } throw new Error(`Failed to retrieve credential from keychain: ${ref.account}`); } } else { throw new Error(`Keychain not supported on platform: ${process.platform}`); } } /** * Reset the cached provider (useful for testing) */ export function resetKeychainProvider(): void { _provider = null; }

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