Skip to main content
Glama
by Coder-RL
config-manager.ts18.4 kB
import { EventEmitter } from 'events'; import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile } from 'fs'; import { join } from 'path'; import * as crypto from 'crypto'; export interface ConfigSource { id: string; type: 'file' | 'env' | 'vault' | 'remote' | 'database'; priority: number; path?: string; url?: string; credentials?: any; refreshInterval?: number; format?: 'json' | 'yaml' | 'env' | 'toml'; encryption?: EncryptionConfig; validation?: ValidationConfig; } export interface EncryptionConfig { enabled: boolean; algorithm: string; key?: string; keyDerivation?: { algorithm: string; salt: string; iterations: number; }; } export interface ValidationConfig { schema?: any; rules?: ValidationRule[]; } export interface ValidationRule { path: string; type: 'required' | 'type' | 'range' | 'regex' | 'custom'; value?: any; message?: string; validator?: (value: any) => boolean | string; } export interface ConfigValue { key: string; value: any; source: string; priority: number; encrypted: boolean; lastModified: Date; metadata?: Record<string, any>; } export interface ConfigTemplate { id: string; name: string; description?: string; variables: TemplateVariable[]; content: string; outputPath?: string; } export interface TemplateVariable { name: string; type: 'string' | 'number' | 'boolean' | 'array' | 'object'; required: boolean; default?: any; description?: string; validation?: ValidationRule; } export class ConfigManager extends EventEmitter { private sources = new Map<string, ConfigSource>(); private values = new Map<string, ConfigValue>(); private templates = new Map<string, ConfigTemplate>(); private watchers = new Map<string, NodeJS.Timeout>(); private secretsCache = new Map<string, { value: any; expires: number }>(); private encryptionKey: Buffer | null = null; private isInitialized = false; constructor(private baseDir: string = process.cwd()) { super(); this.initializeEncryption(); } private initializeEncryption(): void { const keyPath = join(this.baseDir, '.config-key'); if (existsSync(keyPath)) { try { this.encryptionKey = readFileSync(keyPath); } catch (error) { console.warn('Failed to load encryption key:', error); } } else { // Generate new encryption key this.encryptionKey = crypto.randomBytes(32); try { writeFileSync(keyPath, this.encryptionKey, { mode: 0o600 }); } catch (error) { console.warn('Failed to save encryption key:', error); } } } async initialize(): Promise<void> { if (this.isInitialized) { return; } // Load default sources await this.addDefaultSources(); // Load all configurations await this.loadAllConfigurations(); this.isInitialized = true; this.emit('initialized'); } private async addDefaultSources(): Promise<void> { // Environment variables source this.addSource({ id: 'env', type: 'env', priority: 100 }); // Local config files const configFiles = [ { path: 'config.json', format: 'json' as const }, { path: 'config.yaml', format: 'yaml' as const }, { path: '.env', format: 'env' as const } ]; for (const file of configFiles) { const fullPath = join(this.baseDir, file.path); if (existsSync(fullPath)) { this.addSource({ id: `file-${file.format}`, type: 'file', priority: 50, path: fullPath, format: file.format }); } } } addSource(source: ConfigSource): void { this.sources.set(source.id, source); // Set up file watching for file sources if (source.type === 'file' && source.path) { this.watchFile(source); } // Set up refresh interval for remote sources if (source.refreshInterval && source.refreshInterval > 0) { this.setupSourceRefresh(source); } this.emit('source-added', source); } removeSource(sourceId: string): void { const source = this.sources.get(sourceId); if (!source) { return; } // Remove file watcher if (source.type === 'file' && source.path) { unwatchFile(source.path); } // Clear refresh interval const interval = this.watchers.get(sourceId); if (interval) { clearInterval(interval); this.watchers.delete(sourceId); } // Remove values from this source for (const [key, value] of this.values) { if (value.source === sourceId) { this.values.delete(key); } } this.sources.delete(sourceId); this.emit('source-removed', source); } private watchFile(source: ConfigSource): void { if (!source.path) return; watchFile(source.path, { interval: 1000 }, async () => { try { await this.loadConfigurationFromSource(source); this.emit('source-updated', source.id); } catch (error) { this.emit('error', new Error(`Failed to reload config from ${source.path}: ${error}`)); } }); } private setupSourceRefresh(source: ConfigSource): void { const interval = setInterval(async () => { try { await this.loadConfigurationFromSource(source); } catch (error) { this.emit('error', new Error(`Failed to refresh config from ${source.id}: ${error}`)); } }, source.refreshInterval! * 1000); this.watchers.set(source.id, interval); } private async loadAllConfigurations(): Promise<void> { const sources = Array.from(this.sources.values()) .sort((a, b) => b.priority - a.priority); for (const source of sources) { try { await this.loadConfigurationFromSource(source); } catch (error) { this.emit('error', new Error(`Failed to load config from ${source.id}: ${error}`)); } } } private async loadConfigurationFromSource(source: ConfigSource): Promise<void> { let data: Record<string, any> = {}; switch (source.type) { case 'file': data = await this.loadFromFile(source); break; case 'env': data = this.loadFromEnvironment(source); break; case 'vault': data = await this.loadFromVault(source); break; case 'remote': data = await this.loadFromRemote(source); break; case 'database': data = await this.loadFromDatabase(source); break; default: throw new Error(`Unsupported source type: ${source.type}`); } // Decrypt values if needed if (source.encryption?.enabled) { data = this.decryptData(data, source.encryption); } // Validate configuration if (source.validation) { this.validateConfiguration(data, source.validation); } // Store values this.storeValues(data, source); } private async loadFromFile(source: ConfigSource): Promise<Record<string, any>> { if (!source.path || !existsSync(source.path)) { return {}; } const content = readFileSync(source.path, 'utf8'); switch (source.format) { case 'json': return JSON.parse(content); case 'yaml': // In production, use a YAML parser like 'js-yaml' throw new Error('YAML parsing not implemented'); case 'env': return this.parseEnvFile(content); case 'toml': // In production, use a TOML parser throw new Error('TOML parsing not implemented'); default: return JSON.parse(content); } } private loadFromEnvironment(source: ConfigSource): Record<string, any> { const data: Record<string, any> = {}; for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { data[key] = this.parseEnvValue(value); } } return data; } private async loadFromVault(source: ConfigSource): Promise<Record<string, any>> { // Simplified vault client - in production, use actual vault client if (!source.url || !source.credentials) { throw new Error('Vault source requires URL and credentials'); } try { const response = await fetch(`${source.url}/v1/secret/data/config`, { headers: { 'X-Vault-Token': source.credentials.token, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`Vault request failed: ${response.statusText}`); } const result = await response.json(); return result.data?.data || {}; } catch (error) { throw new Error(`Failed to load from vault: ${error}`); } } private async loadFromRemote(source: ConfigSource): Promise<Record<string, any>> { if (!source.url) { throw new Error('Remote source requires URL'); } try { const headers: Record<string, string> = { 'Content-Type': 'application/json' }; if (source.credentials) { if (source.credentials.type === 'bearer') { headers['Authorization'] = `Bearer ${source.credentials.token}`; } else if (source.credentials.type === 'basic') { const auth = Buffer.from(`${source.credentials.username}:${source.credentials.password}`).toString('base64'); headers['Authorization'] = `Basic ${auth}`; } } const response = await fetch(source.url, { headers }); if (!response.ok) { throw new Error(`Remote request failed: ${response.statusText}`); } return await response.json(); } catch (error) { throw new Error(`Failed to load from remote: ${error}`); } } private async loadFromDatabase(source: ConfigSource): Promise<Record<string, any>> { // Simplified database client - in production, use actual database client throw new Error('Database source not implemented'); } private parseEnvFile(content: string): Record<string, any> { const data: Record<string, any> = {}; const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...valueParts] = trimmed.split('='); if (key && valueParts.length > 0) { const value = valueParts.join('=').replace(/^["']|["']$/g, ''); data[key.trim()] = this.parseEnvValue(value); } } } return data; } private parseEnvValue(value: string): any { // Try to parse as JSON if (value.startsWith('{') || value.startsWith('[')) { try { return JSON.parse(value); } catch { return value; } } // Parse booleans if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; // Parse numbers if (/^\d+$/.test(value)) return parseInt(value, 10); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); return value; } private decryptData(data: Record<string, any>, config: EncryptionConfig): Record<string, any> { if (!this.encryptionKey) { throw new Error('Encryption key not available'); } const decrypted: Record<string, any> = {}; for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' && value.startsWith('enc:')) { try { const encrypted = value.substring(4); const [ivHex, encryptedHex] = encrypted.split(':'); const iv = Buffer.from(ivHex, 'hex'); const encryptedBuffer = Buffer.from(encryptedHex, 'hex'); const decipher = crypto.createDecipherGCM(config.algorithm, this.encryptionKey); decipher.setIV(iv); let decryptedValue = decipher.update(encryptedBuffer, undefined, 'utf8'); decryptedValue += decipher.final('utf8'); decrypted[key] = this.parseEnvValue(decryptedValue); } catch (error) { throw new Error(`Failed to decrypt value for key ${key}: ${error}`); } } else { decrypted[key] = value; } } return decrypted; } private validateConfiguration(data: Record<string, any>, config: ValidationConfig): void { if (config.rules) { for (const rule of config.rules) { const value = this.getNestedValue(data, rule.path); const isValid = this.validateRule(value, rule); if (!isValid) { throw new Error(rule.message || `Validation failed for ${rule.path}`); } } } // JSON Schema validation would go here in production if (config.schema) { // Use a library like ajv for JSON schema validation } } private validateRule(value: any, rule: ValidationRule): boolean { switch (rule.type) { case 'required': return value !== undefined && value !== null && value !== ''; case 'type': return typeof value === rule.value; case 'range': if (typeof value === 'number' && Array.isArray(rule.value)) { return value >= rule.value[0] && value <= rule.value[1]; } return false; case 'regex': if (typeof value === 'string' && rule.value instanceof RegExp) { return rule.value.test(value); } return false; case 'custom': return rule.validator ? rule.validator(value) === true : true; default: return true; } } private storeValues(data: Record<string, any>, source: ConfigSource): void { const timestamp = new Date(); for (const [key, value] of Object.entries(data)) { const existingValue = this.values.get(key); // Only update if this source has higher or equal priority if (!existingValue || source.priority >= existingValue.priority) { this.values.set(key, { key, value, source: source.id, priority: source.priority, encrypted: source.encryption?.enabled || false, lastModified: timestamp, metadata: { sourceType: source.type, sourcePath: source.path || source.url } }); this.emit('value-updated', { key, value, source: source.id }); } } } get<T = any>(key: string, defaultValue?: T): T { const configValue = this.values.get(key); if (configValue) { return configValue.value as T; } // Try nested key access const nestedValue = this.getNestedValue(Object.fromEntries( Array.from(this.values.entries()).map(([k, v]) => [k, v.value]) ), key); return nestedValue !== undefined ? nestedValue as T : defaultValue as T; } set(key: string, value: any, sourceId: string = 'runtime'): void { const source = this.sources.get(sourceId) || { id: sourceId, type: 'env' as const, priority: 1000 }; this.values.set(key, { key, value, source: sourceId, priority: source.priority, encrypted: false, lastModified: new Date() }); this.emit('value-updated', { key, value, source: sourceId }); } has(key: string): boolean { return this.values.has(key) || this.getNestedValue( Object.fromEntries(Array.from(this.values.entries()).map(([k, v]) => [k, v.value])), key ) !== undefined; } delete(key: string): boolean { const deleted = this.values.delete(key); if (deleted) { this.emit('value-deleted', { key }); } return deleted; } private getNestedValue(obj: any, path: string): any { return path.split('.').reduce((current, key) => current?.[key], obj); } encrypt(value: string): string { if (!this.encryptionKey) { throw new Error('Encryption key not available'); } const iv = crypto.randomBytes(16); const cipher = crypto.createCipherGCM('aes-256-gcm', this.encryptionKey); cipher.setIV(iv); let encrypted = cipher.update(value, 'utf8', 'hex'); encrypted += cipher.final('hex'); return `enc:${iv.toString('hex')}:${encrypted}`; } addTemplate(template: ConfigTemplate): void { this.templates.set(template.id, template); this.emit('template-added', template); } generateFromTemplate(templateId: string, variables: Record<string, any>): string { const template = this.templates.get(templateId); if (!template) { throw new Error(`Template not found: ${templateId}`); } // Validate required variables for (const variable of template.variables) { if (variable.required && !(variable.name in variables)) { throw new Error(`Required variable missing: ${variable.name}`); } } // Replace variables in template content let content = template.content; for (const variable of template.variables) { const value = variables[variable.name] || variable.default; const placeholder = new RegExp(`\\$\\{${variable.name}\\}`, 'g'); content = content.replace(placeholder, String(value)); } return content; } getAllValues(): Record<string, any> { const result: Record<string, any> = {}; for (const [key, configValue] of this.values) { result[key] = configValue.value; } return result; } getMetadata(key: string): ConfigValue | undefined { return this.values.get(key); } getSources(): ConfigSource[] { return Array.from(this.sources.values()); } getStats(): any { return { sources: this.sources.size, values: this.values.size, templates: this.templates.size, watchers: this.watchers.size, cacheSize: this.secretsCache.size, isInitialized: this.isInitialized }; } async reload(): Promise<void> { this.values.clear(); await this.loadAllConfigurations(); this.emit('reloaded'); } destroy(): void { // Clear all watchers for (const interval of this.watchers.values()) { clearInterval(interval); } // Unwatch all files for (const source of this.sources.values()) { if (source.type === 'file' && source.path) { unwatchFile(source.path); } } this.sources.clear(); this.values.clear(); this.templates.clear(); this.watchers.clear(); this.secretsCache.clear(); this.removeAllListeners(); } }

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/Coder-RL/Claude_MCPServer_Dev1'

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