UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

576 lines (504 loc) 15.9 kB
/** * Encrypted Configuration Management * Provides secure configuration loading with environment-specific encryption */ import path from 'path'; import { SecretsManager } from './secrets-manager.js'; import { logger } from '../logger.js'; export interface ConfigSchema { [key: string]: { type: 'string' | 'number' | 'boolean' | 'object' | 'array'; required?: boolean; default?: any; sensitive?: boolean; validation?: (value: any) => boolean; description?: string; }; } export interface EncryptedConfigOptions { environment: string; configPath: string; secretsPath: string; schema?: ConfigSchema; validateOnLoad?: boolean; watchForChanges?: boolean; } export class EncryptedConfig { private secretsManager: SecretsManager; private config: Record<string, any> = {}; private schema?: ConfigSchema; private environment: string; private options: EncryptedConfigOptions; private watchers: Array<(key: string, value: any, oldValue: any) => void> = []; constructor(options: EncryptedConfigOptions) { this.options = options; this.environment = options.environment; this.schema = options.schema; this.secretsManager = new SecretsManager({ storePath: options.secretsPath, masterKeyPath: path.join(options.secretsPath, 'master.key'), }); } /** * Initialize encrypted configuration */ async initialize(masterPassword?: string): Promise<void> { try { await this.secretsManager.initialize(masterPassword); await this.loadConfiguration(); if (this.options.validateOnLoad && this.schema) { this.validateConfiguration(); } logger.info('Encrypted configuration initialized', { environment: this.environment, configKeys: Object.keys(this.config).length, }); } catch (error) { logger.error('Failed to initialize encrypted configuration', error as Error); throw error; } } /** * Get configuration value */ async get<T = any>(key: string, defaultValue?: T): Promise<T> { try { // Check if value exists in memory cache if (Object.prototype.hasOwnProperty.call(this.config, key)) { return this.config[key] as T; } // Check schema for default value if (this.schema && this.schema[key] && this.schema[key].default !== undefined) { return this.schema[key].default as T; } // Try to load from secrets (for sensitive values) if (this.schema && this.schema[key] && this.schema[key].sensitive) { const secretValue = await this.secretsManager.getSecret(this.getSecretKey(key)); if (secretValue !== null) { const parsedValue = this.parseValue(secretValue, this.schema[key].type); this.config[key] = parsedValue; return parsedValue as T; } } // Return default value if provided if (defaultValue !== undefined) { return defaultValue; } // Check if required in schema if (this.schema && this.schema[key] && this.schema[key].required) { throw new Error(`Required configuration key '${key}' not found`); } return undefined as T; } catch (error) { logger.error('Failed to get configuration value', error as Error, { key }); throw error; } } /** * Set configuration value */ async set(key: string, value: any): Promise<void> { try { // Validate against schema if available if (this.schema && this.schema[key]) { this.validateValue(key, value, this.schema[key]); } const oldValue = this.config[key]; // Store sensitive values in secrets manager if (this.schema && this.schema[key] && this.schema[key].sensitive) { const secretKey = this.getSecretKey(key); const stringValue = this.stringifyValue(value); await this.secretsManager.storeSecret(secretKey, stringValue, { description: this.schema[key].description, tags: ['config', this.environment], }); } else { // Store non-sensitive values in memory this.config[key] = value; } // Notify watchers this.notifyWatchers(key, value, oldValue); logger.debug('Configuration value updated', { key, sensitive: this.schema?.[key]?.sensitive || false, }); } catch (error) { logger.error('Failed to set configuration value', error as Error, { key }); throw error; } } /** * Get multiple configuration values */ async getAll(keys?: string[]): Promise<Record<string, any>> { try { const result: Record<string, any> = {}; const keysToGet = keys || (this.schema ? Object.keys(this.schema) : Object.keys(this.config)); for (const key of keysToGet) { try { result[key] = await this.get(key); } catch (error) { // Continue with other keys if one fails logger.warn('Failed to get configuration key', { key, error: (error as Error).message }); } } return result; } catch (error) { logger.error('Failed to get all configuration values', error as Error); throw error; } } /** * Update multiple configuration values */ async setMany(values: Record<string, any>): Promise<void> { try { const updates = Object.entries(values); for (const [key, value] of updates) { await this.set(key, value); } logger.info('Multiple configuration values updated', { keysUpdated: updates.length, }); } catch (error) { logger.error('Failed to set multiple configuration values', error as Error); throw error; } } /** * Remove configuration value */ async remove(key: string): Promise<boolean> { try { let removed = false; // Remove from secrets if sensitive if (this.schema && this.schema[key] && this.schema[key].sensitive) { const secretKey = this.getSecretKey(key); removed = await this.secretsManager.deleteSecret(secretKey); } // Remove from memory if (Object.prototype.hasOwnProperty.call(this.config, key)) { delete this.config[key]; removed = true; } if (removed) { this.notifyWatchers(key, undefined, this.config[key]); logger.debug('Configuration value removed', { key }); } return removed; } catch (error) { logger.error('Failed to remove configuration value', error as Error, { key }); throw error; } } /** * Watch for configuration changes */ watch(callback: (key: string, value: any, oldValue: any) => void): void { this.watchers.push(callback); } /** * Unwatch configuration changes */ unwatch(callback: (key: string, value: any, oldValue: any) => void): void { const index = this.watchers.indexOf(callback); if (index > -1) { this.watchers.splice(index, 1); } } /** * Validate entire configuration against schema */ validateConfiguration(): void { if (!this.schema) return; const errors: string[] = []; for (const [key, definition] of Object.entries(this.schema)) { try { if (definition.required && !Object.prototype.hasOwnProperty.call(this.config, key)) { // Check if it's available as a secret if (!definition.sensitive) { errors.push(`Required configuration key '${key}' is missing`); } } if (Object.prototype.hasOwnProperty.call(this.config, key)) { this.validateValue(key, this.config[key], definition); } } catch (error) { errors.push(`Validation failed for '${key}': ${(error as Error).message}`); } } if (errors.length > 0) { throw new Error(`Configuration validation failed: ${errors.join(', ')}`); } } /** * Export configuration (excluding sensitive values) */ async exportConfig(includeSensitive: boolean = false): Promise<Record<string, any>> { try { const exported: Record<string, any> = {}; if (this.schema) { for (const [key, definition] of Object.entries(this.schema)) { if (definition.sensitive && !includeSensitive) { exported[key] = '[REDACTED]'; } else { exported[key] = await this.get(key); } } } else { // Export all non-sensitive values from memory for (const [key, value] of Object.entries(this.config)) { exported[key] = value; } } return exported; } catch (error) { logger.error('Failed to export configuration', error as Error); throw error; } } /** * Reload configuration from storage */ async reload(): Promise<void> { try { this.config = {}; await this.loadConfiguration(); logger.info('Configuration reloaded', { environment: this.environment, }); } catch (error) { logger.error('Failed to reload configuration', error as Error); throw error; } } /** * Get configuration schema */ getSchema(): ConfigSchema | undefined { return this.schema; } /** * Update configuration schema */ updateSchema(schema: ConfigSchema): void { this.schema = schema; if (this.options.validateOnLoad) { this.validateConfiguration(); } logger.info('Configuration schema updated', { keys: Object.keys(schema).length, }); } /** * Check if configuration has a specific key */ async has(key: string): Promise<boolean> { try { const value = await this.get(key); return value !== undefined; } catch { return false; } } /** * Get configuration statistics */ async getStats(): Promise<{ totalKeys: number; sensitiveKeys: number; memoryKeys: number; secretKeys: number; environment: string; }> { const sensitiveKeys = this.schema ? Object.values(this.schema).filter(def => def.sensitive).length : 0; const secrets = await this.secretsManager.listSecrets(['config', this.environment]); const secretKeys = secrets.filter(secret => secret.name.startsWith(`config_${this.environment}_`) ).length; return { totalKeys: this.schema ? Object.keys(this.schema).length : Object.keys(this.config).length, sensitiveKeys, memoryKeys: Object.keys(this.config).length, secretKeys, environment: this.environment, }; } /** * Load configuration from various sources */ private async loadConfiguration(): Promise<void> { // Load from environment variables await this.loadFromEnvironment(); // Load from file if specified if (this.options.configPath) { await this.loadFromFile(); } // Load sensitive values from secrets manager await this.loadSensitiveValues(); } /** * Load configuration from environment variables */ private async loadFromEnvironment(): Promise<void> { if (!this.schema) return; for (const [key, definition] of Object.entries(this.schema)) { const envKey = this.getEnvironmentKey(key); const envValue = process.env[envKey]; if (envValue !== undefined) { try { const parsedValue = this.parseValue(envValue, definition.type); if (definition.sensitive) { // Store sensitive env values in secrets await this.secretsManager.storeSecret(this.getSecretKey(key), envValue, { description: `Environment variable: ${envKey}`, tags: ['config', 'env', this.environment], }); } else { this.config[key] = parsedValue; } } catch (error) { logger.warn('Failed to parse environment variable', { key: envKey, error: (error as Error).message, }); } } } } /** * Load configuration from file */ private async loadFromFile(): Promise<void> { try { // Implementation would load from YAML/JSON file // For now, we'll skip file loading in this example logger.debug('File configuration loading not implemented'); } catch (error) { logger.error('Failed to load configuration from file', error as Error); } } /** * Load sensitive values from secrets manager */ private async loadSensitiveValues(): Promise<void> { if (!this.schema) return; for (const [key, definition] of Object.entries(this.schema)) { if (definition.sensitive) { try { const secretValue = await this.secretsManager.getSecret(this.getSecretKey(key)); if (secretValue !== null) { const parsedValue = this.parseValue(secretValue, definition.type); this.config[key] = parsedValue; } } catch (error) { logger.debug('Failed to load sensitive configuration value', { key, error: (error as Error).message, }); } } } } /** * Generate secret key for configuration */ private getSecretKey(configKey: string): string { return `config_${this.environment}_${configKey}`; } /** * Generate environment variable key */ private getEnvironmentKey(configKey: string): string { return `CODECRUCIBLE_${configKey.toUpperCase()}`; } /** * Validate a configuration value against schema definition */ private validateValue(key: string, value: any, definition: any): void { // Type validation if (!this.isCorrectType(value, definition.type)) { throw new Error(`Expected ${definition.type}, got ${typeof value}`); } // Custom validation if (definition.validation && !definition.validation(value)) { throw new Error('Custom validation failed'); } } /** * Check if value matches expected type */ private isCorrectType(value: any, expectedType: string): boolean { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': return typeof value === 'boolean'; case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); case 'array': return Array.isArray(value); default: return true; } } /** * Parse string value to correct type */ private parseValue(value: string, type: string): any { switch (type) { case 'string': return value; case 'number': { const num = Number(value); if (isNaN(num)) throw new Error(`Cannot parse '${value}' as number`); return num; } case 'boolean': return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); case 'object': case 'array': try { return JSON.parse(value); } catch { throw new Error(`Cannot parse '${value}' as JSON`); } default: return value; } } /** * Convert value to string for storage */ private stringifyValue(value: any): string { if (typeof value === 'string') { return value; } return JSON.stringify(value); } /** * Notify watchers of configuration changes */ private notifyWatchers(key: string, value: any, oldValue: any): void { for (const watcher of this.watchers) { try { watcher(key, value, oldValue); } catch (error) { logger.error('Configuration watcher error', error as Error, { key }); } } } /** * Clean up resources */ async stop(): Promise<void> { await this.secretsManager.stop(); this.watchers = []; this.config = {}; logger.info('Encrypted configuration stopped'); } }