Skip to main content
Glama
windows.ts7.51 kB
/** * Windows Credential Manager Provider * * Stores credentials in Windows Credential Manager using cmdkey and PowerShell. * No external dependencies required - uses built-in Windows tools. * * Storage scheme: * - Target: 'wpnav-example.com' (prefix + site domain) * - Username: 'wpnav' (constant) * - Password: the Application Password * * @package WP_Navigator_MCP * @since 2.7.0 */ import { execFileSync } from 'child_process'; import { BaseKeychainProvider } from './keychain.js'; import type { StoredCredential } from './types.js'; import { KEYCHAIN_SERVICE } from './types.js'; /** Prefix for Windows credential targets */ const WINDOWS_TARGET_PREFIX = 'wpnav-'; /** * Windows Credential Manager implementation * * Uses: * - `cmdkey` for store/delete/list (built into Windows) * - PowerShell for retrieve (cmdkey can't output passwords) * * All commands use execFileSync (not exec) to prevent shell injection. */ export class WindowsCredentialProvider extends BaseKeychainProvider { readonly name = 'Windows Credential Manager'; /** * Check if Windows Credential Manager is available */ get available(): boolean { // Only available on Windows if (process.platform !== 'win32') { return false; } // Check if cmdkey exists try { execFileSync('cmdkey.exe', ['/?'], { stdio: 'pipe', timeout: 5000, windowsHide: true, }); return true; } catch { return false; } } /** * Store a credential in Windows Credential Manager * * Uses: cmdkey /generic:wpnav-example.com /user:wpnav /pass:xxx */ async store(credential: StoredCredential): Promise<void> { this.validateAccount(credential.account); this.validatePassword(credential.password); const target = WINDOWS_TARGET_PREFIX + credential.account; try { // First delete any existing credential to avoid duplicates try { execFileSync('cmdkey.exe', ['/delete:' + target], { stdio: 'pipe', timeout: 10000, windowsHide: true, }); } catch { // Ignore - credential may not exist } // Store the new credential execFileSync( 'cmdkey.exe', ['/generic:' + target, '/user:' + KEYCHAIN_SERVICE, '/pass:' + credential.password], { stdio: 'pipe', timeout: 10000, windowsHide: true, } ); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); throw new Error(`Failed to store credential in Windows Credential Manager: ${errMsg}`); } } /** * Retrieve a password from Windows Credential Manager * * Uses PowerShell with .NET CredentialManager API since cmdkey * cannot output passwords directly. * * Uses execFileSync with PowerShell to prevent shell injection. * * @returns The password or null if not found */ async retrieve(account: string): Promise<string | null> { this.validateAccount(account); const target = WINDOWS_TARGET_PREFIX + account; // PowerShell script to read credential using .NET Windows API // Uses P/Invoke to call CredReadW directly - no external modules needed 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 { // Use execFileSync with PowerShell - pass script via -Command argument // This is safe because we validated the account name and use single quotes in the script 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(); return password || null; } catch { // Not found or access denied return null; } } /** * Delete a credential from Windows Credential Manager * * Uses: cmdkey /delete:wpnav-example.com * * @returns true if deleted, false if not found */ async delete(account: string): Promise<boolean> { this.validateAccount(account); const target = WINDOWS_TARGET_PREFIX + account; try { execFileSync('cmdkey.exe', ['/delete:' + target], { stdio: 'pipe', timeout: 10000, windowsHide: true, }); return true; } catch { // Not found or access denied return false; } } /** * List all stored wpnav credentials * * Parses output of: cmdkey /list * Filters entries with target starting with 'wpnav-' * * @returns Array of account names (site domains) */ async list(): Promise<string[]> { try { const result = execFileSync('cmdkey.exe', ['/list'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, windowsHide: true, }); const accounts: string[] = []; const lines = (result as string).split('\n'); // Parse output format: // Target: LegacyGeneric:target=wpnav-example.com // or Target: wpnav-example.com for (const line of lines) { const trimmed = line.trim(); // Look for Target: lines if (trimmed.toLowerCase().startsWith('target:')) { const targetPart = trimmed.substring(7).trim(); // Extract the actual target name // Format can be: "LegacyGeneric:target=wpnav-xxx" or just "wpnav-xxx" let targetName = targetPart; const legacyMatch = targetPart.match(/LegacyGeneric:target=(.+)/i); if (legacyMatch) { targetName = legacyMatch[1]; } // Check if it's a wpnav credential if (targetName.startsWith(WINDOWS_TARGET_PREFIX)) { const account = targetName.substring(WINDOWS_TARGET_PREFIX.length); if (account.length >= 3) { accounts.push(account); } } } } // Dedupe and sort return [...new Set(accounts)].sort(); } catch { // If cmdkey fails, return empty list return []; } } }

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