package config import ( "fmt" "os" "path/filepath" "strconv" ) type Config struct { // Server Port string // Database DatabaseURL string // Storage RepoRoot string ArtifactRoot 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 // GitOps GitOpsReconcileInterval int // seconds between periodic drift checks; 0 disables // Federation InstanceURL string InstanceName string // Artifact signing (Phase 4) // PEM-encoded ECDSA P-256 private key. If empty an ephemeral key is generated. ArtifactSigningKey string // OCI Registry OCIRoot string // SSH server SSHHost string // env: SSH_HOST, empty = auto-detect from request/instance URL SSHPort string // env: SSH_PORT, default "2222" SSHHostKeyPath string // env: SSH_HOST_KEY_PATH, empty = generate ephemeral // Dev Debug bool } func Load() (*Config, error) { repoRoot := getEnv("REPO_ROOT", "/var/lib/forgebucket/repos") cfg := &Config{ Port: getEnv("PORT", "8080"), RepoRoot: repoRoot, ArtifactRoot: getEnv("ARTIFACT_ROOT", filepath.Join(filepath.Dir(repoRoot), "artifacts")), Debug: getEnvBool("DEBUG", false), NATSUrl: getEnv("NATS_URL", ""), GitOpsReconcileInterval: getEnvInt("GITOPS_RECONCILE_INTERVAL", 300), 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) cfg.SSHHost = os.Getenv("SSH_HOST") cfg.SSHPort = getEnv("SSH_PORT", "2222") cfg.SSHHostKeyPath = os.Getenv("SSH_HOST_KEY_PATH") // Optional signing key cfg.ArtifactSigningKey = os.Getenv("ARTIFACT_SIGNING_KEY") cfg.OCIRoot = getEnv("OCI_ROOT", filepath.Join(filepath.Dir(cfg.RepoRoot), "oci")) // 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 getEnvInt(key string, fallback int) int { v := os.Getenv(key) if v == "" { return fallback } n, err := strconv.Atoi(v) if err != nil { return fallback } return n } 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 }