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:
50
.env.example
50
.env.example
@@ -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
87
src/config/index.ts
Normal 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
181
src/interfaces/index.ts
Normal 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
139
src/middleware/index.ts
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user