Skip to main content
Glama
macos.ts5.57 kB
/** * macOS Keychain Provider * * Stores credentials in macOS Keychain using the built-in `security` command. * No external dependencies required. * * Storage scheme: * - Account: 'wpnav' (constant for all entries) * - Service: site domain (e.g., 'example.com') * - 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'; /** * macOS Keychain implementation * * Uses the `security` command-line tool (built into macOS). * Stores credentials in the user's login keychain. */ export class MacOSKeychainProvider extends BaseKeychainProvider { readonly name = 'macOS Keychain'; /** * Check if macOS Keychain is available */ get available(): boolean { // Only available on macOS if (process.platform !== 'darwin') { return false; } // Check if security command exists try { execFileSync('security', ['help'], { stdio: 'pipe', timeout: 5000, }); return true; } catch { return false; } } /** * Store a credential in macOS Keychain * * Uses: security add-generic-password -a wpnav -s <site> -w <password> -U * * The -U flag updates if exists, creates if not. */ async store(credential: StoredCredential): Promise<void> { this.validateAccount(credential.account); this.validatePassword(credential.password); try { // -a: account name (we use 'wpnav' as constant) // -s: service name (we use site domain) // -w: password // -U: update if exists, create if not execFileSync( 'security', [ 'add-generic-password', '-a', KEYCHAIN_SERVICE, '-s', credential.account, '-w', credential.password, '-U', ], { stdio: 'pipe', timeout: 10000, } ); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); // Check for common errors if (errMsg.includes('User canceled')) { throw new Error('Keychain access was denied by user'); } throw new Error(`Failed to store credential in keychain: ${errMsg}`); } } /** * Retrieve a password from macOS Keychain * * Uses: security find-generic-password -a wpnav -s <site> -w * * @returns The password or null if not found */ async retrieve(account: string): Promise<string | null> { this.validateAccount(account); try { // -a: account name // -s: service name // -w: output password only (not full keychain entry) const result = execFileSync( 'security', ['find-generic-password', '-a', KEYCHAIN_SERVICE, '-s', account, '-w'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, } ); return result.trim(); } catch { // Not found or access denied - return null return null; } } /** * Delete a credential from macOS Keychain * * Uses: security delete-generic-password -a wpnav -s <site> * * @returns true if deleted, false if not found */ async delete(account: string): Promise<boolean> { this.validateAccount(account); try { execFileSync('security', ['delete-generic-password', '-a', KEYCHAIN_SERVICE, '-s', account], { stdio: 'pipe', timeout: 10000, }); return true; } catch { // Not found or access denied return false; } } /** * List all stored wpnav credentials * * Parses output of: security dump-keychain * * This is more complex as we need to filter entries. * Returns empty array if parsing fails. */ async list(): Promise<string[]> { try { // dump-keychain outputs all keychain entries // We filter for entries with acct="wpnav" const result = execFileSync('security', ['dump-keychain'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000, maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large keychains }); const accounts: string[] = []; const lines = result.split('\n'); // Track current entry's service name let currentService: string | null = null; let isGenericPassword = false; for (const line of lines) { // New entry starts with "keychain:" if (line.startsWith('keychain:')) { currentService = null; isGenericPassword = false; } // Check if this is a generic password entry if (line.includes('"genp"') || line.includes('class: "genp"')) { isGenericPassword = true; } // Parse service name: "svce"<blob>="example.com" const svceMatch = line.match(/"svce"<blob>="([^"]+)"/); if (svceMatch) { currentService = svceMatch[1]; } // Parse account name: "acct"<blob>="wpnav" const acctMatch = line.match(/"acct"<blob>="([^"]+)"/); if (acctMatch && acctMatch[1] === KEYCHAIN_SERVICE && currentService && isGenericPassword) { accounts.push(currentService); currentService = null; } } // Dedupe and sort return [...new Set(accounts)].sort(); } catch { // If dump-keychain 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