Enhance configuration management with comprehensive environment setup

- Added detailed .env.example with extensive configuration options
- Created centralized configuration module with type-safe environment variable loading
- Implemented configuration validation and default value handling
- Added support for different environment modes (development, production, test)
- Introduced interfaces for robust type definitions across the project
- Implemented middleware for request validation, security, and input sanitization
This commit is contained in:
jango-blockchained
2025-02-03 00:32:14 +01:00
parent f81cb1af62
commit cee8cf8b1e
4 changed files with 455 additions and 4 deletions

View File

@@ -1,7 +1,51 @@
NODE_ENV=development
# Home Assistant Configuration
# The URL of your Home Assistant instance
HASS_HOST=http://homeassistant.local:8123
# Long-lived access token from Home Assistant
# Generate from Profile -> Long-Lived Access Tokens
HASS_TOKEN=your_home_assistant_token
PORT=3000
# WebSocket URL for real-time updates
HASS_SOCKET_URL=ws://homeassistant.local:8123/api/websocket
LOG_LEVEL=debug
# Server Configuration
# Port for the MCP server (default: 3000)
PORT=3000
# Environment (development/production/test)
NODE_ENV=development
# Debug mode (true/false)
DEBUG=false
# Logging level (debug/info/warn/error)
LOG_LEVEL=info
# AI Configuration
# Natural Language Processor type (claude/gpt4/custom)
PROCESSOR_TYPE=claude
# OpenAI API Key (required for GPT-4 analysis)
OPENAI_API_KEY=your_openai_api_key
# Rate Limiting
# Requests per minute per IP for regular endpoints
RATE_LIMIT_REGULAR=100
# Requests per minute per IP for WebSocket connections
RATE_LIMIT_WEBSOCKET=1000
# Security
# JWT secret for token generation (change this in production!)
JWT_SECRET=your_jwt_secret_key
# CORS configuration (comma-separated list of allowed origins)
CORS_ORIGINS=http://localhost:3000,http://localhost:8123
# Test Configuration
# Only needed if running tests
TEST_HASS_HOST=http://localhost:8123
TEST_HASS_TOKEN=test_token
TEST_HASS_SOCKET_URL=ws://localhost:8123/api/websocket
TEST_PORT=3001

87
src/config/index.ts Normal file
View File

@@ -0,0 +1,87 @@
import { config } from 'dotenv';
import { resolve } from 'path';
// Load environment variables based on NODE_ENV
const envFile = process.env.NODE_ENV === 'production'
? '.env'
: process.env.NODE_ENV === 'test'
? '.env.test'
: '.env.development';
console.log(`Loading environment from ${envFile}`);
config({ path: resolve(process.cwd(), envFile) });
// Home Assistant Configuration
export const HASS_CONFIG = {
HOST: process.env.HASS_HOST || 'http://homeassistant.local:8123',
TOKEN: process.env.HASS_TOKEN,
SOCKET_URL: process.env.HASS_SOCKET_URL || 'ws://homeassistant.local:8123/api/websocket'
};
// Server Configuration
export const SERVER_CONFIG = {
PORT: parseInt(process.env.PORT || '3000', 10),
NODE_ENV: process.env.NODE_ENV || 'development',
DEBUG: process.env.DEBUG === 'true',
LOG_LEVEL: process.env.LOG_LEVEL || 'info'
};
// AI Configuration
export const AI_CONFIG = {
PROCESSOR_TYPE: process.env.PROCESSOR_TYPE || 'claude',
OPENAI_API_KEY: process.env.OPENAI_API_KEY
};
// Rate Limiting Configuration
export const RATE_LIMIT_CONFIG = {
REGULAR: parseInt(process.env.RATE_LIMIT_REGULAR || '100', 10),
WEBSOCKET: parseInt(process.env.RATE_LIMIT_WEBSOCKET || '1000', 10)
};
// Security Configuration
export const SECURITY_CONFIG = {
JWT_SECRET: process.env.JWT_SECRET || 'default_secret_key_change_in_production',
CORS_ORIGINS: (process.env.CORS_ORIGINS || 'http://localhost:3000,http://localhost:8123')
.split(',')
.map(origin => origin.trim())
};
// Test Configuration
export const TEST_CONFIG = {
HASS_HOST: process.env.TEST_HASS_HOST || 'http://localhost:8123',
HASS_TOKEN: process.env.TEST_HASS_TOKEN || 'test_token',
HASS_SOCKET_URL: process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket',
PORT: parseInt(process.env.TEST_PORT || '3001', 10)
};
// Mock Configuration (for testing)
export const MOCK_CONFIG = {
SERVICES: process.env.MOCK_SERVICES === 'true',
RESPONSES_DIR: process.env.MOCK_RESPONSES_DIR || '__tests__/mock-responses'
};
// Validate required configuration
function validateConfig() {
const missingVars: string[] = [];
if (!HASS_CONFIG.TOKEN) missingVars.push('HASS_TOKEN');
if (!SECURITY_CONFIG.JWT_SECRET) missingVars.push('JWT_SECRET');
if (missingVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
}
}
// Export configuration validation
export const validateConfiguration = validateConfig;
// Export all configurations as a single object
export const AppConfig = {
HASS: HASS_CONFIG,
SERVER: SERVER_CONFIG,
AI: AI_CONFIG,
RATE_LIMIT: RATE_LIMIT_CONFIG,
SECURITY: SECURITY_CONFIG,
TEST: TEST_CONFIG,
MOCK: MOCK_CONFIG
};

181
src/interfaces/index.ts Normal file
View File

@@ -0,0 +1,181 @@
import { z } from 'zod';
// Tool interfaces
export interface Tool {
name: string;
description: string;
parameters: z.ZodType<any>;
execute: (params: any) => Promise<any>;
}
// Command interfaces
export interface CommandParams {
command: string;
entity_id: string;
// Common parameters
state?: string;
// Light parameters
brightness?: number;
color_temp?: number;
rgb_color?: [number, number, number];
// Cover parameters
position?: number;
tilt_position?: number;
// Climate parameters
temperature?: number;
target_temp_high?: number;
target_temp_low?: number;
hvac_mode?: string;
fan_mode?: string;
humidity?: number;
}
// Home Assistant interfaces
export interface HassEntity {
entity_id: string;
state: string;
attributes: Record<string, any>;
last_changed?: string;
last_updated?: string;
context?: {
id: string;
parent_id?: string;
user_id?: string;
};
}
export interface HassState {
entity_id: string;
state: string;
attributes: {
friendly_name?: string;
description?: string;
[key: string]: any;
};
}
export interface HassAddon {
name: string;
slug: string;
description: string;
version: string;
installed: boolean;
available: boolean;
state: string;
}
export interface HassAddonResponse {
data: {
addons: HassAddon[];
};
}
export interface HassAddonInfoResponse {
data: {
name: string;
slug: string;
description: string;
version: string;
state: string;
status: string;
options: Record<string, any>;
[key: string]: any;
};
}
// HACS interfaces
export interface HacsRepository {
name: string;
description: string;
category: string;
installed: boolean;
version_installed: string;
available_version: string;
authors: string[];
domain: string;
}
export interface HacsResponse {
repositories: HacsRepository[];
}
// Automation interfaces
export interface AutomationConfig {
alias: string;
description?: string;
mode?: 'single' | 'parallel' | 'queued' | 'restart';
trigger: any[];
condition?: any[];
action: any[];
}
export interface AutomationResponse {
automation_id: string;
}
// SSE interfaces
export interface SSEHeaders {
onAbort?: () => void;
}
export interface SSEParams {
token: string;
events?: string[];
entity_id?: string;
domain?: string;
}
// History interfaces
export interface HistoryParams {
entity_id: string;
start_time?: string;
end_time?: string;
minimal_response?: boolean;
significant_changes_only?: boolean;
}
// Scene interfaces
export interface SceneParams {
action: 'list' | 'activate';
scene_id?: string;
}
// Notification interfaces
export interface NotifyParams {
message: string;
title?: string;
target?: string;
data?: Record<string, any>;
}
// Automation parameter interfaces
export interface AutomationParams {
action: 'list' | 'toggle' | 'trigger';
automation_id?: string;
}
export interface AddonParams {
action: 'list' | 'info' | 'install' | 'uninstall' | 'start' | 'stop' | 'restart';
slug?: string;
version?: string;
}
export interface PackageParams {
action: 'list' | 'install' | 'uninstall' | 'update';
category: 'integration' | 'plugin' | 'theme' | 'python_script' | 'appdaemon' | 'netdaemon';
repository?: string;
version?: string;
}
export interface AutomationConfigParams {
action: 'create' | 'update' | 'delete' | 'duplicate';
automation_id?: string;
config?: {
alias: string;
description?: string;
mode?: 'single' | 'parallel' | 'queued' | 'restart';
trigger: any[];
condition?: any[];
action: any[];
};
}

139
src/middleware/index.ts Normal file
View File

@@ -0,0 +1,139 @@
import { Request, Response, NextFunction } from 'express';
import { HASS_CONFIG, RATE_LIMIT_CONFIG } from '../config/index.js';
import rateLimit from 'express-rate-limit';
// Rate limiter middleware
export const rateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: RATE_LIMIT_CONFIG.REGULAR,
message: {
success: false,
message: 'Too many requests, please try again later.',
reset_time: new Date(Date.now() + 60 * 1000).toISOString()
}
});
// WebSocket rate limiter middleware
export const wsRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: RATE_LIMIT_CONFIG.WEBSOCKET,
message: {
success: false,
message: 'Too many WebSocket connections, please try again later.',
reset_time: new Date(Date.now() + 60 * 1000).toISOString()
}
});
// Security headers middleware
export const securityHeaders = (_req: Request, res: Response, next: NextFunction) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
};
// Request validation middleware
export const validateRequest = (req: Request, res: Response, next: NextFunction) => {
// Validate content type for POST/PUT/PATCH requests
if (['POST', 'PUT', 'PATCH'].includes(req.method) && !req.is('application/json')) {
return res.status(415).json({
success: false,
message: 'Content-Type must be application/json'
});
}
// Validate request body size
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
if (contentLength > 1024 * 1024) { // 1MB limit
return res.status(413).json({
success: false,
message: 'Request body too large'
});
}
next();
};
// Input sanitization middleware
export const sanitizeInput = (req: Request, _res: Response, next: NextFunction) => {
if (req.body) {
// Recursively sanitize object
const sanitizeObject = (obj: any): any => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => sanitizeObject(item));
}
const sanitized: any = {};
for (const [key, value] of Object.entries(obj)) {
// Remove any potentially dangerous characters from keys
const sanitizedKey = key.replace(/[<>]/g, '');
sanitized[sanitizedKey] = sanitizeObject(value);
}
return sanitized;
};
req.body = sanitizeObject(req.body);
}
next();
};
// Authentication middleware
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || token !== HASS_CONFIG.TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
next();
};
// Error handling middleware
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Error:', err);
// Handle specific error types
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: 'Validation error',
details: err.message
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
success: false,
message: 'Unauthorized',
details: err.message
});
}
// Default error response
res.status(500).json({
success: false,
message: 'Internal server error',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
});
};
// Export all middleware
export const middleware = {
rateLimiter,
wsRateLimiter,
securityHeaders,
validateRequest,
sanitizeInput,
authenticate,
errorHandler
};