Skip to main content
Glama
config.ts6.78 kB
import * as fs from 'fs'; import * as path from 'path'; import * as toml from 'toml'; import { homedir } from 'os'; import { isLocalhostAddress } from './core/security-utils.js'; export interface SpecWorkflowConfig { projectDir?: string; port?: number; bindAddress?: string; // IP address to bind to (e.g., '127.0.0.1', '0.0.0.0') allowExternalAccess?: boolean; // Explicit opt-in for non-localhost binding dashboardOnly?: boolean; lang?: string; // Security features security?: { rateLimitEnabled?: boolean; rateLimitPerMinute?: number; auditLogEnabled?: boolean; auditLogPath?: string; auditLogRetentionDays?: number; corsEnabled?: boolean; allowedOrigins?: string[]; }; } export interface ConfigLoadResult { config: SpecWorkflowConfig | null; configPath: string | null; error?: string; } function expandTilde(filepath: string): string { if (filepath.startsWith('~')) { return path.join(homedir(), filepath.slice(1)); } return filepath; } function validatePort(port: number): boolean { return Number.isInteger(port) && port >= 1024 && port <= 65535; } function validateConfig(config: any): { valid: boolean; error?: string } { if (config.port !== undefined) { if (!validatePort(config.port)) { return { valid: false, error: `Invalid port: ${config.port}. Port must be between 1024 and 65535.` }; } } if (config.projectDir !== undefined && typeof config.projectDir !== 'string') { return { valid: false, error: `Invalid projectDir: must be a string.` }; } if (config.dashboardOnly !== undefined && typeof config.dashboardOnly !== 'boolean') { return { valid: false, error: `Invalid dashboardOnly: must be a boolean.` }; } if (config.lang !== undefined && typeof config.lang !== 'string') { return { valid: false, error: `Invalid lang: must be a string.` }; } // Validate network configuration if (config.bindAddress !== undefined && typeof config.bindAddress !== 'string') { return { valid: false, error: `Invalid bindAddress: must be a valid IP address string.` }; } if (config.allowExternalAccess !== undefined && typeof config.allowExternalAccess !== 'boolean') { return { valid: false, error: `Invalid allowExternalAccess: must be a boolean.` }; } // Network security validation: if binding to non-localhost address, require explicit allowExternalAccess if (config.bindAddress !== undefined && !isLocalhostAddress(config.bindAddress) && !config.allowExternalAccess) { return { valid: false, error: `Network security: binding to '${config.bindAddress}' (non-localhost) requires explicit allowExternalAccess = true. This exposes your dashboard to network access.` }; } // Validate security features if (config.security !== undefined) { const sec = config.security; if (sec.rateLimitPerMinute !== undefined && (typeof sec.rateLimitPerMinute !== 'number' || sec.rateLimitPerMinute < 1)) { return { valid: false, error: `Invalid security.rateLimitPerMinute: must be a positive number.` }; } if (sec.auditLogRetentionDays !== undefined && (typeof sec.auditLogRetentionDays !== 'number' || sec.auditLogRetentionDays < 1)) { return { valid: false, error: `Invalid security.auditLogRetentionDays: must be a positive number.` }; } } return { valid: true }; } export function loadConfigFromPath(configPath: string): ConfigLoadResult { try { const expandedPath = expandTilde(configPath); if (!fs.existsSync(expandedPath)) { return { config: null, configPath: expandedPath, error: `Config file not found: ${expandedPath}` }; } const configContent = fs.readFileSync(expandedPath, 'utf-8'); const parsedConfig = toml.parse(configContent); const validation = validateConfig(parsedConfig); if (!validation.valid) { return { config: null, configPath: expandedPath, error: validation.error }; } const config: SpecWorkflowConfig = {}; if (parsedConfig.projectDir !== undefined) { config.projectDir = expandTilde(parsedConfig.projectDir); } if (parsedConfig.port !== undefined) { config.port = parsedConfig.port; } if (parsedConfig.bindAddress !== undefined) { config.bindAddress = parsedConfig.bindAddress; } if (parsedConfig.allowExternalAccess !== undefined) { config.allowExternalAccess = parsedConfig.allowExternalAccess; } if (parsedConfig.dashboardOnly !== undefined) { config.dashboardOnly = parsedConfig.dashboardOnly; } if (parsedConfig.lang !== undefined) { config.lang = parsedConfig.lang; } if (parsedConfig.security !== undefined) { config.security = parsedConfig.security; } return { config, configPath: expandedPath }; } catch (error) { if (error instanceof Error) { return { config: null, configPath: null, error: `Failed to load config file: ${error.message}` }; } return { config: null, configPath: null, error: 'Failed to load config file: Unknown error' }; } } export function loadConfigFile(projectDir: string, customConfigPath?: string): ConfigLoadResult { // If custom config path is provided, use it if (customConfigPath) { return loadConfigFromPath(customConfigPath); } // Otherwise, look for default config in project directory try { const expandedDir = expandTilde(projectDir); const configDir = path.join(expandedDir, '.spec-workflow'); const configPath = path.join(configDir, 'config.toml'); if (!fs.existsSync(configPath)) { return { config: null, configPath: null }; } return loadConfigFromPath(configPath); } catch (error) { if (error instanceof Error) { return { config: null, configPath: null, error: `Failed to load config file: ${error.message}` }; } return { config: null, configPath: null, error: 'Failed to load config file: Unknown error' }; } } export function mergeConfigs( fileConfig: SpecWorkflowConfig | null, cliArgs: Partial<SpecWorkflowConfig> ): SpecWorkflowConfig { const merged: SpecWorkflowConfig = {}; if (fileConfig) { Object.assign(merged, fileConfig); } Object.keys(cliArgs).forEach(key => { const value = cliArgs[key as keyof SpecWorkflowConfig]; if (value !== undefined) { (merged as any)[key] = value; } }); return merged; }

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/Pimzino/spec-workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server