package config import ( "errors" "fmt" "os" "path/filepath" "time" ) // Config holds runtime configuration for the alexa-bridge process. type Config struct { HTTPPort string // HTTP_PORT, default "8080" AlexaSkillID string // ALEXA_SKILL_ID — verified against the request envelope's application ID (required) HAGatewayAddr string // HA_GATEWAY_ADDR, default "ha-gateway.home-services.svc.cluster.local:50051" HAGatewayServerName string // HA_GATEWAY_SERVER_NAME, default "ha-gateway.home-services.svc.cluster.local" TLSDir string // TLS_DIR, default "/tls" — unlike ai-gateway/discord-bot, mTLS is on by default here since alexa-bridge is internet-facing; set empty to disable for local dev EntityRefreshInterval time.Duration // ENTITY_REFRESH_INTERVAL, default 5m OTELEndpoint string // OTEL_ENDPOINT, empty disables telemetry LogLevel string // LOG_LEVEL, default "info" LogFormat string // LOG_FORMAT, default "json" } // Load reads configuration from environment variables and applies defaults. func Load() (*Config, error) { skillID := os.Getenv("ALEXA_SKILL_ID") if skillID == "" { return nil, errors.New("ALEXA_SKILL_ID is required but not set") } refreshInterval, err := parseDurationEnv("ENTITY_REFRESH_INTERVAL", 5*time.Minute) if err != nil { return nil, err } cfg := &Config{ HTTPPort: getenvDefault("HTTP_PORT", "8080"), AlexaSkillID: skillID, HAGatewayAddr: getenvDefault("HA_GATEWAY_ADDR", "ha-gateway.home-services.svc.cluster.local:50051"), HAGatewayServerName: getenvDefault("HA_GATEWAY_SERVER_NAME", "ha-gateway.home-services.svc.cluster.local"), TLSDir: getenvDefault("TLS_DIR", "/tls"), EntityRefreshInterval: refreshInterval, OTELEndpoint: os.Getenv("OTEL_ENDPOINT"), LogLevel: getenvDefault("LOG_LEVEL", "info"), LogFormat: getenvDefault("LOG_FORMAT", "json"), } if cfg.TLSDir != "" { if err := validateTLSDir(cfg.TLSDir); err != nil { return nil, err } } return cfg, nil } func getenvDefault(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func parseDurationEnv(key string, fallback time.Duration) (time.Duration, error) { if v := os.Getenv(key); v != "" { d, err := time.ParseDuration(v) if err != nil { return 0, fmt.Errorf("parse %s: %w", key, err) } return d, nil } return fallback, nil } func validateTLSDir(dir string) error { required := []string{"tls.crt", "tls.key", "ca.crt"} for _, name := range required { path := filepath.Join(dir, name) info, err := os.Stat(path) if err != nil { return fmt.Errorf("tls dir validation failed for %s: %w", path, err) } if info.IsDir() { return fmt.Errorf("tls dir validation failed for %s: %w", path, errors.New("expected file")) } } return nil }