package config import ( "fmt" "os" "strconv" ) type Config struct { // Server Port string // Database DatabaseURL string // Storage RepoRoot string // Security SessionSecret string // must be 32 or 64 bytes for AES-GCM CSRFSecret string // must be 32 bytes // OIDC (optional) OIDCIssuer string OIDCClientID string OIDCClientSecret string // Event bus NATSUrl string // Federation InstanceURL string InstanceName string // Dev Debug bool } func Load() (*Config, error) { cfg := &Config{ Port: getEnv("PORT", "8080"), RepoRoot: getEnv("REPO_ROOT", "/var/lib/forgebucket/repos"), Debug: getEnvBool("DEBUG", false), NATSUrl: getEnv("NATS_URL", ""), InstanceURL: getEnv("INSTANCE_URL", ""), InstanceName: getEnv("INSTANCE_NAME", "ForgeBucket"), } var missing []string cfg.DatabaseURL = requireEnv("DATABASE_URL", &missing) cfg.SessionSecret = requireEnv("SESSION_SECRET", &missing) cfg.CSRFSecret = requireEnv("CSRF_SECRET", &missing) // Optional OIDC cfg.OIDCIssuer = os.Getenv("OIDC_ISSUER") cfg.OIDCClientID = os.Getenv("OIDC_CLIENT_ID") cfg.OIDCClientSecret = os.Getenv("OIDC_CLIENT_SECRET") if len(missing) > 0 { return nil, fmt.Errorf("missing required env vars: %v", missing) } if len(cfg.SessionSecret) < 32 { return nil, fmt.Errorf("SESSION_SECRET must be at least 32 characters") } if len(cfg.CSRFSecret) != 32 { return nil, fmt.Errorf("CSRF_SECRET must be exactly 32 characters") } return cfg, nil } func requireEnv(key string, missing *[]string) string { v := os.Getenv(key) if v == "" { *missing = append(*missing, key) } return v } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getEnvBool(key string, fallback bool) bool { v := os.Getenv(key) if v == "" { return fallback } b, err := strconv.ParseBool(v) if err != nil { return fallback } return b }