import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { AuthConfig, AuthMode } from '../types/index.js';
import { getHelperSecret, getHelperPort } from './credential-helper.js';
interface HostConfig {
username?: string;
password?: string;
keyPath?: string;
}
interface ConfigFile {
hosts?: Record<string, HostConfig>;
defaults?: HostConfig;
}
function loadConfig(): ConfigFile {
const configPath = join(homedir(), '.ssh', 'mcp-hosts.json');
if (!existsSync(configPath)) return {};
try {
return JSON.parse(readFileSync(configPath, 'utf8'));
} catch {
return {};
}
}
async function requestFromHelper(host: string, authMode: AuthMode, username?: string): Promise<AuthConfig | null> {
const port = getHelperPort();
if (!port) return null;
try {
const res = await fetch(`http://127.0.0.1:${port}/credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getHelperSecret()}`
},
body: JSON.stringify({ host, auth_mode: authMode, username }),
signal: AbortSignal.timeout(120000), // 2 min for user input
});
if (!res.ok) return null;
const data = await res.json() as any;
if (data.error) return null;
return { username: data.username, password: data.password, keyPath: data.key_path };
} catch {
return null;
}
}
export async function getCredentials(
host: string,
authMode: AuthMode,
provided?: Partial<AuthConfig>
): Promise<{ status: 'ready' | 'awaiting_credentials'; credentials?: AuthConfig; message?: string }> {
const config = loadConfig();
const hostConfig = config.hosts?.[host] || {};
const defaults = config.defaults || {};
// Priority: provided > host config > defaults > env vars
const creds: Partial<AuthConfig> = {
username: provided?.username || hostConfig.username || defaults.username || process.env.SSH_USER,
password: provided?.password || hostConfig.password || defaults.password || process.env.SSH_PASSWORD,
keyPath: provided?.keyPath || hostConfig.keyPath || defaults.keyPath || process.env.SSH_KEY_PATH,
};
const hasRequired = authMode === 'password'
? creds.username && creds.password
: creds.username && creds.keyPath;
if (hasRequired) {
if (authMode === 'key' && creds.keyPath && !existsSync(creds.keyPath)) {
return { status: 'awaiting_credentials', message: `Key file not found: ${creds.keyPath}` };
}
return { status: 'ready', credentials: creds as AuthConfig };
}
// Try GUI credential helper
const port = getHelperPort();
if (port) {
const helperCreds = await requestFromHelper(host, authMode, creds.username);
if (helperCreds) {
if (authMode === 'key' && helperCreds.keyPath && !existsSync(helperCreds.keyPath)) {
return { status: 'awaiting_credentials', message: `Key file not found: ${helperCreds.keyPath}` };
}
return { status: 'ready', credentials: helperCreds };
}
return { status: 'awaiting_credentials', message: 'Credential prompt was cancelled' };
}
return {
status: 'awaiting_credentials',
message: 'No credentials. Set SSH_USER/SSH_PASSWORD env vars or ensure GUI (zenity/osascript) is available.'
};
}