package config import ( "fmt" "os" "strings" "time" ) // Config is the runtime configuration, loaded from environment variables. type Config struct { Env string Debug bool HTTPPort string PublicBase string // external base URL, used for cookies AllowOrigins []string DB struct { Driver string // "sqlite" (dev default) or "postgres" DSN string } Redis struct { Addr string Password string Enabled bool } Auth struct { AccessTokenTTL time.Duration RefreshTokenTTL time.Duration JWTSecret string RefreshCookieName string RefreshCookieSecure bool // false for local http dev; set true behind TLS RefreshCookieSameSite string } // MasterKey encrypts channel API keys at rest (AES-GCM). MasterKey string Proxy struct { DefaultMaxTokens int DefaultTimeoutMs int BillingExactBalance bool // reject when estimated cost > balance MaxRetries int // additional channel attempts on transport/5xx failures } Registration struct { Mode string // "open" | "invite" } HealthCheck struct { Interval time.Duration MaxFailures int Cooldown time.Duration TimeoutMs int TestModel string } RateLimit struct { RequestsPerMin int // per user global limiter Burst int } MetricsEnabled bool } func Load() *Config { c := &Config{} c.Env = get("APP_ENV", "development") c.Debug = strings.EqualFold(get("DEBUG", "false"), "true") c.HTTPPort = get("HTTP_PORT", "8080") c.PublicBase = get("PUBLIC_BASE", "http://localhost:8080") if o := get("CORS_ORIGINS", "*"); o != "*" { c.AllowOrigins = strings.Split(o, ",") } else { c.AllowOrigins = []string{"*"} } c.DB.Driver = get("DB_DRIVER", "sqlite") if c.DB.Driver == "postgres" { c.DB.DSN = get("DATABASE_URL", "host=localhost user=postgres password=postgres dbname=openteam port=5432 sslmode=disable") } else { path := get("SQLITE_PATH", "data/openteam.db") c.DB.DSN = path } c.Redis.Addr = get("REDIS_ADDR", "localhost:6379") c.Redis.Password = get("REDIS_PASSWORD", "") c.Redis.Enabled = strings.EqualFold(get("REDIS_ENABLED", "false"), "true") c.Auth.AccessTokenTTL = duration(get("ACCESS_TOKEN_TTL", "2h"), 2*time.Hour) c.Auth.RefreshTokenTTL = duration(get("REFRESH_TOKEN_TTL", "168h"), 7*24*time.Hour) c.Auth.JWTSecret = get("JWT_SECRET", "dev-only-secret-change-me") c.Auth.RefreshCookieName = get("REFRESH_COOKIE_NAME", "ot_refresh") c.Auth.RefreshCookieSecure = strings.EqualFold(get("REFRESH_COOKIE_SECURE", "false"), "true") c.Auth.RefreshCookieSameSite = get("REFRESH_COOKIE_SAMESITE", "lax") c.MasterKey = get("MASTER_KEY", "dev-only-master-key-change-me") c.Proxy.DefaultMaxTokens = intVal(get("DEFAULT_MAX_TOKENS", "4096"), 4096) c.Proxy.DefaultTimeoutMs = intVal(get("PROXY_TIMEOUT_MS", "300000"), 300000) c.Proxy.BillingExactBalance = strings.EqualFold(get("BILLING_EXACT_BALANCE", "false"), "true") c.Proxy.MaxRetries = intVal(get("PROXY_MAX_RETRIES", "1"), 1) c.Registration.Mode = get("REGISTRATION_MODE", "open") c.HealthCheck.Interval = duration(get("HEALTHCHECK_INTERVAL", "60s"), time.Minute) c.HealthCheck.MaxFailures = intVal(get("HEALTHCHECK_MAX_FAILURES", "3"), 3) c.HealthCheck.Cooldown = duration(get("HEALTHCHECK_COOLDOWN", "300s"), 5*time.Minute) c.HealthCheck.TimeoutMs = intVal(get("HEALTHCHECK_TIMEOUT_MS", "15000"), 15000) c.HealthCheck.TestModel = get("HEALTHCHECK_TEST_MODEL", "") c.RateLimit.RequestsPerMin = intVal(get("RATE_LIMIT_PER_MIN", "60"), 60) c.RateLimit.Burst = intVal(get("RATE_LIMIT_BURST", "120"), 120) c.MetricsEnabled = strings.EqualFold(get("METRICS_ENABLED", "false"), "true") return c } func get(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func intVal(s string, def int) int { n := 0 if _, err := fmt.Sscanf(s, "%d", &n); err != nil || n <= 0 { return def } return n } func duration(s string, def time.Duration) time.Duration { d, err := time.ParseDuration(s) if err != nil || d <= 0 { return def } return d }