import os from 'node:os'; import path from 'node:path'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function envString(key: string, fallback: string): string { return process.env[key] ?? fallback; } function envInt(key: string, fallback: number): number { const raw = process.env[key]; if (raw === undefined) return fallback; const parsed = Number.parseInt(raw, 10); if (Number.isNaN(parsed)) { // eslint-disable-next-line no-console console.error(`[config] Invalid integer for ${key}="${raw}", using default ${fallback}`); return fallback; } return parsed; } function envBool(key: string, fallback: boolean): boolean { const raw = process.env[key]; if (raw === undefined) return fallback; // Accept common truthy / falsy strings if (['true', '1', 'yes'].includes(raw.toLowerCase())) return true; if (['false', '0', 'no'].includes(raw.toLowerCase())) return false; return fallback; } // --------------------------------------------------------------------------- // HOST safety check — must run before exporting config // --------------------------------------------------------------------------- const host = envString('HOST', '127.0.0.1'); if (host === '0.0.0.0' || host === '::') { const allow = process.env['ALLOW_REMOTE']; if (allow !== 'yes-i-understand-the-risk') { // Use console.error directly — the logger module depends on config, // so it is not available yet at this point. // eslint-disable-next-line no-console console.error( `[FATAL] HOST is set to "${host}" which exposes the service to the network.\n` + `If you really intend to do this, set ALLOW_REMOTE=yes-i-understand-the-risk\n` + `Refusing to start.`, ); process.exit(1); } } // --------------------------------------------------------------------------- // Operation timeouts (milliseconds) // Matches the tiers described in PLAN.md section 6.1 // --------------------------------------------------------------------------- const operationTimeouts: Record = { like: 15_000, // 15s — quick interactions favorite: 15_000, // 15s comment: 20_000, // 20s reply: 20_000, // 20s feed_list: 30_000, // 30s — page load + extraction search: 30_000, // 30s feed_detail: 60_000, // 60s — includes scroll loading user_profile: 60_000, // 60s publish: 300_000, // 5min — upload may be slow login: 300_000, // 5min — user interaction default: 60_000, // 1min — fallback }; // --------------------------------------------------------------------------- // Config type // --------------------------------------------------------------------------- export interface AppConfig { /** HTTP port */ port: number; /** HTTP bind address */ host: string; /** Run browser in headless mode */ headless: boolean; /** Custom browser executable path (optional) */ browserBin: string | undefined; /** Pino log level */ logLevel: string; /** NODE_ENV */ nodeEnv: string; /** Directory for per-platform cookie storage */ cookieDir: string; /** Max pending operations per platform queue */ maxQueueDepth: number; /** Per-operation-type timeout in ms */ operationTimeouts: Record; } // --------------------------------------------------------------------------- // Exported config singleton // --------------------------------------------------------------------------- export const config: AppConfig = { port: envInt('PORT', 9527), host, headless: envBool('HEADLESS', true), browserBin: process.env['BROWSER_BIN'] || undefined, logLevel: envString('LOG_LEVEL', 'info'), nodeEnv: envString('NODE_ENV', 'development'), cookieDir: envString('COOKIE_DIR', path.join(os.homedir(), '.social-mcp')), maxQueueDepth: envInt('MAX_QUEUE_DEPTH', 10), operationTimeouts, };