diff --git a/config/unified.config.ts b/config/unified.config.ts new file mode 100644 index 0000000..9ecf825 --- /dev/null +++ b/config/unified.config.ts @@ -0,0 +1,947 @@ +/** + * Unified Application Configuration System + * + * This file serves as the single source of truth for all application configuration. + * It supports multiple environments (development, staging, production) and provides + * type-safe configuration for both frontend and backend components. + * + * Configuration sources (in order of precedence): + * 1. Environment variables + * 2. Environment-specific config files + * 3. Default values + */ + +export type Environment = 'development' | 'staging' | 'production' | 'test'; + +export interface UnifiedConfig { + // Application Identity + app: { + name: string; + version: string; + environment: Environment; + baseUrl: string; + port: number; + }; + + // Database Configuration + database: { + url: string; + username: string; + password: string; + name: string; + useMock: boolean; + connectionTimeout: number; + retryAttempts: number; + }; + + // Container & Registry Configuration + container: { + registry: string; + repository: string; + tag: string; + imageUrl: string; + }; + + // Kubernetes Configuration + kubernetes: { + namespace: string; + ingressHost: string; + ingressClass: string; + certIssuer: string; + storageClass: string; + storageSize: string; + replicas: { + frontend: number; + database: number; + }; + resources: { + frontend: { + requests: { memory: string; cpu: string }; + limits: { memory: string; cpu: string }; + }; + database: { + requests: { memory: string; cpu: string }; + limits: { memory: string; cpu: string }; + }; + }; + }; + + // Authentication Configuration + auth: { + jwtSecret: string; + jwtExpiresIn: string; + refreshTokenExpiresIn: string; + emailVerificationExpiresIn: string; + bcryptRounds: number; + }; + + // Email Configuration + email: { + provider: 'mailgun' | 'smtp' | 'console'; + mailgun?: { + apiKey: string; + domain: string; + baseUrl: string; + }; + smtp?: { + host: string; + port: number; + secure: boolean; + username: string; + password: string; + }; + fromName: string; + fromEmail: string; + }; + + // OAuth Configuration + oauth: { + google?: { + clientId: string; + clientSecret?: string; + }; + github?: { + clientId: string; + clientSecret?: string; + }; + }; + + // Feature Flags + features: { + enableEmailVerification: boolean; + enableOAuth: boolean; + enableAdminInterface: boolean; + enableMonitoring: boolean; + enableMetrics: boolean; + enableTracing: boolean; + enableRateLimiting: boolean; + enableSecurityHeaders: boolean; + debugMode: boolean; + hotReload: boolean; + }; + + // Performance & Monitoring + performance: { + cacheTimeout: number; + requestTimeout: number; + maxConnections: number; + enableCors: boolean; + corsOrigin: string | string[]; + }; + + // Logging Configuration + logging: { + level: 'debug' | 'info' | 'warn' | 'error'; + format: 'json' | 'text'; + enableTimestamp: boolean; + enableColors: boolean; + }; + + // Security Configuration + security: { + enableHttps: boolean; + enableHsts: boolean; + enableCsp: boolean; + sessionSecret: string; + rateLimitRequests: number; + rateLimitWindow: number; + }; +} + +/** + * Environment-specific configuration overrides + */ +const environmentConfigs: Record> = { + development: { + app: { + name: 'rxminder', + version: '1.0.0', + environment: 'development', + baseUrl: 'http://localhost:5173', + port: 5173, + }, + database: { + url: 'http://localhost:5984', + username: 'admin', + password: 'password', + name: 'rxminder_dev', + useMock: false, + connectionTimeout: 5000, + retryAttempts: 3, + }, + kubernetes: { + namespace: 'rxminder-dev', + ingressHost: 'rxminder-dev.local', + ingressClass: 'nginx', + certIssuer: 'selfsigned', + storageClass: 'standard', + storageSize: '1Gi', + replicas: { + frontend: 1, + database: 1, + }, + resources: { + frontend: { + requests: { memory: '128Mi', cpu: '50m' }, + limits: { memory: '256Mi', cpu: '200m' }, + }, + database: { + requests: { memory: '256Mi', cpu: '100m' }, + limits: { memory: '512Mi', cpu: '500m' }, + }, + }, + }, + features: { + enableEmailVerification: false, + enableOAuth: false, + enableAdminInterface: true, + enableMonitoring: false, + enableMetrics: false, + enableTracing: false, + enableRateLimiting: false, + enableSecurityHeaders: false, + debugMode: true, + hotReload: true, + }, + logging: { + level: 'debug', + format: 'text', + enableTimestamp: true, + enableColors: true, + }, + performance: { + cacheTimeout: 300, // 5 minutes + requestTimeout: 30000, + maxConnections: 100, + enableCors: true, + corsOrigin: '*', + }, + }, + + staging: { + app: { + name: 'rxminder', + version: '1.0.0', + environment: 'staging', + baseUrl: 'https://staging.rxminder.com', + port: 80, + }, + kubernetes: { + namespace: 'rxminder-staging', + ingressHost: 'staging.rxminder.com', + ingressClass: 'nginx', + certIssuer: 'letsencrypt-staging', + storageClass: 'ssd', + storageSize: '5Gi', + replicas: { + frontend: 2, + database: 1, + }, + resources: { + frontend: { + requests: { memory: '256Mi', cpu: '100m' }, + limits: { memory: '512Mi', cpu: '500m' }, + }, + database: { + requests: { memory: '512Mi', cpu: '200m' }, + limits: { memory: '1Gi', cpu: '1000m' }, + }, + }, + }, + features: { + enableEmailVerification: true, + enableOAuth: true, + enableAdminInterface: true, + enableMonitoring: true, + enableMetrics: true, + enableTracing: false, + enableRateLimiting: true, + enableSecurityHeaders: true, + debugMode: false, + hotReload: false, + }, + logging: { + level: 'info', + format: 'json', + enableTimestamp: true, + enableColors: false, + }, + performance: { + cacheTimeout: 1800, // 30 minutes + requestTimeout: 60000, + maxConnections: 150, + enableCors: true, + corsOrigin: 'https://staging.rxminder.com', + }, + }, + + production: { + app: { + name: 'rxminder', + version: '1.0.0', + environment: 'production', + baseUrl: 'https://rxminder.com', + port: 80, + }, + database: { + url: 'http://rxminder-couchdb-service:5984', + username: 'admin', + password: 'secure_password', + name: 'rxminder_prod', + useMock: false, + connectionTimeout: 10000, + retryAttempts: 5, + }, + kubernetes: { + namespace: 'rxminder-prod', + ingressHost: 'rxminder.com', + ingressClass: 'nginx', + certIssuer: 'letsencrypt-prod', + storageClass: 'ssd', + storageSize: '10Gi', + replicas: { + frontend: 3, + database: 1, + }, + resources: { + frontend: { + requests: { memory: '256Mi', cpu: '100m' }, + limits: { memory: '512Mi', cpu: '500m' }, + }, + database: { + requests: { memory: '512Mi', cpu: '200m' }, + limits: { memory: '1Gi', cpu: '1000m' }, + }, + }, + }, + features: { + enableEmailVerification: true, + enableOAuth: true, + enableAdminInterface: true, + enableMonitoring: true, + enableMetrics: true, + enableTracing: true, + enableRateLimiting: true, + enableSecurityHeaders: true, + debugMode: false, + hotReload: false, + }, + logging: { + level: 'warn', + format: 'json', + enableTimestamp: true, + enableColors: false, + }, + performance: { + cacheTimeout: 3600, // 1 hour + requestTimeout: 120000, + maxConnections: 200, + enableCors: true, + corsOrigin: 'https://rxminder.com', + }, + security: { + enableHttps: true, + enableHsts: true, + enableCsp: true, + sessionSecret: 'secure-session-secret-key', + rateLimitRequests: 100, + rateLimitWindow: 900000, // 15 minutes + }, + }, + + test: { + app: { + name: 'rxminder', + version: '1.0.0', + environment: 'test', + baseUrl: 'http://localhost:3000', + port: 3000, + }, + database: { + url: 'http://localhost:5984', + username: 'test', + password: 'test', + name: 'rxminder_test', + useMock: true, + connectionTimeout: 3000, + retryAttempts: 1, + }, + features: { + enableEmailVerification: false, + enableOAuth: false, + enableAdminInterface: false, + enableMonitoring: false, + enableMetrics: false, + enableTracing: false, + enableRateLimiting: false, + enableSecurityHeaders: false, + debugMode: true, + hotReload: false, + }, + logging: { + level: 'error', + format: 'text', + enableTimestamp: false, + enableColors: false, + }, + email: { + provider: 'console' as const, + fromName: 'Test RxMinder', + fromEmail: 'test@rxminder.local', + }, + }, +}; + +/** + * Default configuration values + */ +const defaultConfig: UnifiedConfig = { + app: { + name: 'RxMinder', + version: '1.0.0', + environment: 'development', + baseUrl: 'http://localhost:5173', + port: 5173, + }, + + database: { + url: 'http://localhost:5984', + username: 'admin', + password: 'changeme', + name: 'meds_app', + useMock: false, + connectionTimeout: 30000, + retryAttempts: 3, + }, + + container: { + registry: 'gitea-http.taildb3494.ts.net', + repository: 'will/meds', + tag: 'latest', + imageUrl: '', // Will be computed + }, + + kubernetes: { + namespace: 'rxminder', + ingressHost: 'rxminder.local', + ingressClass: 'nginx', + certIssuer: 'letsencrypt-prod', + storageClass: 'longhorn', + storageSize: '1Gi', + replicas: { + frontend: 1, + database: 1, + }, + resources: { + frontend: { + requests: { memory: '128Mi', cpu: '50m' }, + limits: { memory: '256Mi', cpu: '200m' }, + }, + database: { + requests: { memory: '256Mi', cpu: '100m' }, + limits: { memory: '512Mi', cpu: '500m' }, + }, + }, + }, + + auth: { + jwtSecret: 'your-super-secret-jwt-key-change-in-production', + jwtExpiresIn: '1h', + refreshTokenExpiresIn: '7d', + emailVerificationExpiresIn: '24h', + bcryptRounds: 12, + }, + + email: { + provider: 'console', + mailgun: { + apiKey: '', + domain: '', + baseUrl: 'https://api.mailgun.net/v3', + }, + fromName: 'RxMinder', + fromEmail: 'noreply@rxminder.com', + }, + + oauth: { + google: { + clientId: '', + }, + github: { + clientId: '', + }, + }, + + features: { + enableEmailVerification: true, + enableOAuth: true, + enableAdminInterface: true, + enableMonitoring: false, + enableMetrics: false, + enableTracing: false, + enableRateLimiting: false, + enableSecurityHeaders: false, + debugMode: false, + hotReload: false, + }, + + performance: { + cacheTimeout: 1800, + requestTimeout: 30000, + maxConnections: 100, + enableCors: true, + corsOrigin: '*', + }, + + logging: { + level: 'info', + format: 'json', + enableTimestamp: true, + enableColors: false, + }, + + security: { + enableHttps: false, + enableHsts: false, + enableCsp: false, + sessionSecret: 'your-session-secret-change-in-production', + rateLimitRequests: 100, + rateLimitWindow: 900000, // 15 minutes + }, +}; + +/** + * Get environment variable with fallback + */ +function getEnvVar(key: string, fallback: string = ''): string { + if (typeof process !== 'undefined' && process.env) { + return process.env[key] || fallback; + } + if (typeof import.meta !== 'undefined' && import.meta.env) { + return import.meta.env[key] || fallback; + } + return fallback; +} + +/** + * Get boolean environment variable + */ +function getBoolEnvVar(key: string, fallback: boolean = false): boolean { + const value = getEnvVar(key); + if (value === '') return fallback; + return value.toLowerCase() === 'true' || value === '1'; +} + +/** + * Get number environment variable + */ +function getNumberEnvVar(key: string, fallback: number = 0): number { + const value = getEnvVar(key); + if (value === '') return fallback; + const parsed = parseInt(value, 10); + return isNaN(parsed) ? fallback : parsed; +} + +/** + * Load configuration from environment variables + */ +function loadFromEnvironment(config: UnifiedConfig): UnifiedConfig { + return { + ...config, + app: { + ...config.app, + name: getEnvVar('APP_NAME', config.app.name), + version: getEnvVar('APP_VERSION', config.app.version), + baseUrl: getEnvVar('APP_BASE_URL', config.app.baseUrl), + port: getNumberEnvVar('PORT', config.app.port), + }, + + database: { + ...config.database, + url: + getEnvVar('VITE_COUCHDB_URL') || + getEnvVar('COUCHDB_URL', config.database.url), + username: + getEnvVar('VITE_COUCHDB_USER') || + getEnvVar('COUCHDB_USER', config.database.username), + password: + getEnvVar('VITE_COUCHDB_PASSWORD') || + getEnvVar('COUCHDB_PASSWORD', config.database.password), + name: getEnvVar('COUCHDB_DATABASE_NAME', config.database.name), + useMock: getBoolEnvVar('USE_MOCK_DB', config.database.useMock), + }, + + container: { + ...config.container, + registry: getEnvVar('CONTAINER_REGISTRY', config.container.registry), + repository: getEnvVar( + 'CONTAINER_REPOSITORY', + config.container.repository + ), + tag: getEnvVar('CONTAINER_TAG', config.container.tag), + }, + + kubernetes: { + ...config.kubernetes, + ingressHost: getEnvVar('INGRESS_HOST', config.kubernetes.ingressHost), + storageClass: getEnvVar('STORAGE_CLASS', config.kubernetes.storageClass), + storageSize: getEnvVar('STORAGE_SIZE', config.kubernetes.storageSize), + }, + + auth: { + ...config.auth, + jwtSecret: getEnvVar('JWT_SECRET', config.auth.jwtSecret), + jwtExpiresIn: getEnvVar('JWT_EXPIRES_IN', config.auth.jwtExpiresIn), + }, + + security: { + ...config.security, + sessionSecret: getEnvVar('SESSION_SECRET', config.security.sessionSecret), + }, + + email: { + ...config.email, + provider: + (getEnvVar('EMAIL_PROVIDER', config.email.provider) as + | 'mailgun' + | 'smtp' + | 'console') || config.email.provider, + mailgun: { + ...config.email.mailgun!, + apiKey: + getEnvVar('VITE_MAILGUN_API_KEY') || + getEnvVar('MAILGUN_API_KEY', config.email.mailgun?.apiKey || ''), + domain: + getEnvVar('VITE_MAILGUN_DOMAIN') || + getEnvVar('MAILGUN_DOMAIN', config.email.mailgun?.domain || ''), + }, + fromName: + getEnvVar('VITE_MAILGUN_FROM_NAME') || + getEnvVar('MAILGUN_FROM_NAME', config.email.fromName), + fromEmail: + getEnvVar('VITE_MAILGUN_FROM_EMAIL') || + getEnvVar('MAILGUN_FROM_EMAIL', config.email.fromEmail), + }, + + oauth: { + google: { + clientId: + getEnvVar('VITE_GOOGLE_CLIENT_ID') || + getEnvVar('GOOGLE_CLIENT_ID', config.oauth.google?.clientId || ''), + clientSecret: getEnvVar('GOOGLE_CLIENT_SECRET'), + }, + github: { + clientId: + getEnvVar('VITE_GITHUB_CLIENT_ID') || + getEnvVar('GITHUB_CLIENT_ID', config.oauth.github?.clientId || ''), + clientSecret: getEnvVar('GITHUB_CLIENT_SECRET'), + }, + }, + + features: { + ...config.features, + enableEmailVerification: getBoolEnvVar( + 'ENABLE_EMAIL_VERIFICATION', + config.features.enableEmailVerification + ), + enableOAuth: getBoolEnvVar('ENABLE_OAUTH', config.features.enableOAuth), + enableAdminInterface: getBoolEnvVar( + 'ENABLE_ADMIN_INTERFACE', + config.features.enableAdminInterface + ), + enableMonitoring: getBoolEnvVar( + 'ENABLE_MONITORING', + config.features.enableMonitoring + ), + enableMetrics: getBoolEnvVar( + 'ENABLE_METRICS', + config.features.enableMetrics + ), + debugMode: getBoolEnvVar('DEBUG_MODE', config.features.debugMode), + }, + + logging: { + ...config.logging, + level: + (getEnvVar('LOG_LEVEL', config.logging.level) as + | 'debug' + | 'info' + | 'warn' + | 'error') || config.logging.level, + }, + }; +} + +/** + * Deep merge two objects + */ +function deepMerge(target: T, source: Partial): T { + const result = { ...target }; + for (const key in source) { + if ( + source[key] && + typeof source[key] === 'object' && + !Array.isArray(source[key]) + ) { + result[key as keyof T] = deepMerge( + result[key as keyof T], + source[key] as Partial + ); + } else if (source[key] !== undefined) { + result[key as keyof T] = source[key] as T[keyof T]; + } + } + return result; +} + +/** + * Validate configuration + */ +function validateConfig(config: UnifiedConfig): void { + const errors: string[] = []; + const warnings: string[] = []; + + // Production-specific validations + if (config.app.environment === 'production') { + if (config.auth.jwtSecret === defaultConfig.auth.jwtSecret) { + errors.push('JWT_SECRET must be changed in production'); + } + if ( + config.security.sessionSecret === defaultConfig.security.sessionSecret + ) { + errors.push('SESSION_SECRET must be changed in production'); + } + if (config.database.password === defaultConfig.database.password) { + warnings.push( + 'Consider changing default database password in production' + ); + } + } + + // Development warnings for default secrets + if (config.app.environment === 'development') { + if (config.auth.jwtSecret === defaultConfig.auth.jwtSecret) { + warnings.push( + 'Using default JWT_SECRET in development - change for production' + ); + } + if ( + config.security.sessionSecret === defaultConfig.security.sessionSecret + ) { + warnings.push( + 'Using default SESSION_SECRET in development - change for production' + ); + } + } + + // Email configuration validation + if ( + config.features.enableEmailVerification && + config.email.provider === 'mailgun' + ) { + if (!config.email.mailgun?.apiKey) { + warnings.push( + 'MAILGUN_API_KEY not configured - email will fall back to console' + ); + } + if (!config.email.mailgun?.domain) { + warnings.push('MAILGUN_DOMAIN not configured - email may not work'); + } + } + + // OAuth configuration validation + if (config.features.enableOAuth) { + if (!config.oauth.google?.clientId && !config.oauth.github?.clientId) { + warnings.push('No OAuth client IDs configured - OAuth will be disabled'); + } + } + + // Log warnings and throw errors + if (warnings.length > 0) { + console.warn('⚠️ Configuration warnings:', warnings); + } + if (errors.length > 0) { + console.error('❌ Configuration errors:', errors); + throw new Error(`Configuration validation failed: ${errors.join(', ')}`); + } +} + +/** + * Create unified configuration + */ +export function createUnifiedConfig(): UnifiedConfig { + // Determine environment + const nodeEnv = getEnvVar('NODE_ENV', 'development') as Environment; + const environment = ['development', 'staging', 'production', 'test'].includes( + nodeEnv + ) + ? (nodeEnv as Environment) + : 'development'; + + return createUnifiedConfigForEnvironment(environment); +} + +/** + * Create unified configuration for specific environment + */ +export function createUnifiedConfigForEnvironment( + environment: Environment +): UnifiedConfig { + // Start with default config + let config = { ...defaultConfig }; + config.app.environment = environment; + + // Apply environment-specific overrides + if (environmentConfigs[environment]) { + config = deepMerge(config, environmentConfigs[environment]); + } + + // Apply environment variable overrides + config = loadFromEnvironment(config); + + // Compute derived values + config.container.imageUrl = `${config.container.registry}/${config.container.repository}:${config.container.tag}`; + + // Validate configuration + if (environment !== 'test') { + validateConfig(config); + } + + return config; +} + +/** + * Singleton configuration instance + */ +export const unifiedConfig = createUnifiedConfig(); + +/** + * Export specific configuration sections for convenience + */ +export const appConfig = unifiedConfig.app; +export const databaseConfig = unifiedConfig.database; +export const containerConfig = unifiedConfig.container; +export const kubernetesConfig = unifiedConfig.kubernetes; +export const authConfig = unifiedConfig.auth; +export const emailConfig = unifiedConfig.email; +export const oauthConfig = unifiedConfig.oauth; +export const featureFlags = unifiedConfig.features; +export const performanceConfig = unifiedConfig.performance; +export const loggingConfig = unifiedConfig.logging; +export const securityConfig = unifiedConfig.security; + +/** + * Utility functions + */ +export const isProduction = () => + unifiedConfig.app.environment === 'production'; +export const isDevelopment = () => + unifiedConfig.app.environment === 'development'; +export const isStaging = () => unifiedConfig.app.environment === 'staging'; +export const isTest = () => unifiedConfig.app.environment === 'test'; + +/** + * Export configuration as environment variables (for scripts and Kubernetes) + */ +export function exportAsEnvVars( + config?: UnifiedConfig +): Record { + const configToUse = config || unifiedConfig; + + return { + // Application + APP_NAME: configToUse.app.name, + APP_VERSION: configToUse.app.version, + NODE_ENV: configToUse.app.environment, + APP_BASE_URL: configToUse.app.baseUrl, + PORT: configToUse.app.port.toString(), + + // Database + COUCHDB_URL: configToUse.database.url, + COUCHDB_USER: configToUse.database.username, + COUCHDB_PASSWORD: configToUse.database.password, + COUCHDB_DATABASE_NAME: configToUse.database.name, + USE_MOCK_DB: configToUse.database.useMock.toString(), + + // Container + CONTAINER_REGISTRY: configToUse.container.registry, + CONTAINER_REPOSITORY: configToUse.container.repository, + CONTAINER_TAG: configToUse.container.tag, + DOCKER_IMAGE: configToUse.container.imageUrl, + + // Kubernetes + KUBERNETES_NAMESPACE: configToUse.kubernetes.namespace, + INGRESS_HOST: configToUse.kubernetes.ingressHost, + INGRESS_CLASS: configToUse.kubernetes.ingressClass, + CERT_MANAGER_ISSUER: configToUse.kubernetes.certIssuer, + STORAGE_CLASS: configToUse.kubernetes.storageClass, + STORAGE_SIZE: configToUse.kubernetes.storageSize, + + // Features + ENABLE_EMAIL_VERIFICATION: + configToUse.features.enableEmailVerification.toString(), + ENABLE_OAUTH: configToUse.features.enableOAuth.toString(), + ENABLE_ADMIN_INTERFACE: + configToUse.features.enableAdminInterface.toString(), + ENABLE_MONITORING: configToUse.features.enableMonitoring.toString(), + ENABLE_METRICS: configToUse.features.enableMetrics.toString(), + DEBUG_MODE: configToUse.features.debugMode.toString(), + + // Logging + LOG_LEVEL: configToUse.logging.level, + LOG_FORMAT: configToUse.logging.format, + + // Performance + CACHE_TTL: configToUse.performance.cacheTimeout.toString(), + REQUEST_TIMEOUT: configToUse.performance.requestTimeout.toString(), + MAX_CONNECTIONS: configToUse.performance.maxConnections.toString(), + ENABLE_CORS: configToUse.performance.enableCors.toString(), + CORS_ORIGIN: Array.isArray(configToUse.performance.corsOrigin) + ? configToUse.performance.corsOrigin.join(',') + : configToUse.performance.corsOrigin, + + // Replicas (for Kubernetes) + FRONTEND_REPLICAS: configToUse.kubernetes.replicas.frontend.toString(), + DATABASE_REPLICAS: configToUse.kubernetes.replicas.database.toString(), + + // Resources (for Kubernetes) + FRONTEND_MEMORY_REQUEST: + configToUse.kubernetes.resources.frontend.requests.memory, + FRONTEND_CPU_REQUEST: + configToUse.kubernetes.resources.frontend.requests.cpu, + FRONTEND_MEMORY_LIMIT: + configToUse.kubernetes.resources.frontend.limits.memory, + FRONTEND_CPU_LIMIT: configToUse.kubernetes.resources.frontend.limits.cpu, + DATABASE_MEMORY_REQUEST: + configToUse.kubernetes.resources.database.requests.memory, + DATABASE_CPU_REQUEST: + configToUse.kubernetes.resources.database.requests.cpu, + DATABASE_MEMORY_LIMIT: + configToUse.kubernetes.resources.database.limits.memory, + DATABASE_CPU_LIMIT: configToUse.kubernetes.resources.database.limits.cpu, + }; +} + +/** + * Debug helper to log current configuration + */ +export function logConfig(): void { + if (unifiedConfig.features.debugMode) { + console.warn('🔧 Unified Configuration:', { + environment: unifiedConfig.app.environment, + app: unifiedConfig.app.name, + version: unifiedConfig.app.version, + baseUrl: unifiedConfig.app.baseUrl, + database: { + url: unifiedConfig.database.url, + useMock: unifiedConfig.database.useMock, + }, + features: unifiedConfig.features, + }); + } +} + +// Auto-log in development +if (isDevelopment() && typeof window !== 'undefined') { + logConfig(); +}