config.go raw

   1  package main
   2  
   3  import (
   4  	"encoding/json"
   5  	"os"
   6  	"path/filepath"
   7  	"strconv"
   8  	"strings"
   9  	"time"
  10  
  11  	"github.com/adrg/xdg"
  12  )
  13  
  14  // ConfigFile is the JSON structure for persistent configuration.
  15  type ConfigFile struct {
  16  	DBBackend    string   `json:"db_backend,omitempty"`
  17  	DBBinary     string   `json:"db_binary,omitempty"`
  18  	RelayBinary  string   `json:"relay_binary,omitempty"`
  19  	ACLBinary    string   `json:"acl_binary,omitempty"`
  20  	DBListen     string   `json:"db_listen,omitempty"`
  21  	ACLListen    string   `json:"acl_listen,omitempty"`
  22  	ACLEnabled   *bool    `json:"acl_enabled,omitempty"`
  23  	ACLMode      string   `json:"acl_mode,omitempty"`
  24  	DataDir      string   `json:"data_dir,omitempty"`
  25  	LogLevel     string   `json:"log_level,omitempty"`
  26  	AdminPort    *int     `json:"admin_port,omitempty"`
  27  	AdminOwners  []string `json:"admin_owners,omitempty"`
  28  	BinDir       string   `json:"bin_dir,omitempty"`
  29  	RelayPort    *int     `json:"relay_port,omitempty"`
  30  	RelayHost    string   `json:"relay_host,omitempty"`
  31  	TLSDomains   string   `json:"tls_domains,omitempty"`
  32  	AuthToWrite  *bool    `json:"auth_to_write,omitempty"`
  33  	AuthRequired *bool    `json:"auth_required,omitempty"`
  34  
  35  	// Sync services
  36  	DistributedSyncEnabled *bool  `json:"distributed_sync_enabled,omitempty"`
  37  	ClusterSyncEnabled     *bool  `json:"cluster_sync_enabled,omitempty"`
  38  	RelayGroupEnabled      *bool  `json:"relay_group_enabled,omitempty"`
  39  	NegentropyEnabled      *bool  `json:"negentropy_enabled,omitempty"`
  40  	NegentropyBinary       string `json:"negentropy_binary,omitempty"`
  41  	NegentropyListen       string `json:"negentropy_listen,omitempty"`
  42  
  43  	// Certificate service
  44  	CertsEnabled *bool  `json:"certs_enabled,omitempty"`
  45  	CertsBinary  string `json:"certs_binary,omitempty"`
  46  
  47  	// Bitcoin node (nits)
  48  	NitsEnabled   *bool  `json:"nits_enabled,omitempty"`
  49  	NitsBinary    string `json:"nits_binary,omitempty"`
  50  	NitsShimBinary string `json:"nits_shim_binary,omitempty"`
  51  	NitsListen    string `json:"nits_listen,omitempty"`
  52  	NitsRPCPort   *int   `json:"nits_rpc_port,omitempty"`
  53  	NitsDataDir   string `json:"nits_data_dir,omitempty"`
  54  	NitsPruneMB   *int   `json:"nits_prune_mb,omitempty"`
  55  	NitsNetwork   string `json:"nits_network,omitempty"`
  56  
  57  	// Lightning node (luk)
  58  	LukEnabled   *bool  `json:"luk_enabled,omitempty"`
  59  	LukBinary    string `json:"luk_binary,omitempty"`
  60  	LukDataDir   string `json:"luk_data_dir,omitempty"`
  61  	LukRPCListen string `json:"luk_rpc_listen,omitempty"`
  62  	LukPeerListen string `json:"luk_peer_listen,omitempty"`
  63  
  64  	// Wallet (strela)
  65  	StrelaEnabled *bool  `json:"strela_enabled,omitempty"`
  66  	StrelaBinary string `json:"strela_binary,omitempty"`
  67  	StrelaPort   *int   `json:"strela_port,omitempty"`
  68  	StrelaDataDir string `json:"strela_data_dir,omitempty"`
  69  }
  70  
  71  // configFilePath returns the path to the config file.
  72  func configFilePath() string {
  73  	return filepath.Join(xdg.ConfigHome, "orly", "launcher.json")
  74  }
  75  
  76  // loadConfigFile loads configuration from the JSON file if it exists.
  77  func loadConfigFile() (*ConfigFile, error) {
  78  	path := configFilePath()
  79  	data, err := os.ReadFile(path)
  80  	if err != nil {
  81  		if os.IsNotExist(err) {
  82  			return &ConfigFile{}, nil
  83  		}
  84  		return nil, err
  85  	}
  86  
  87  	var cf ConfigFile
  88  	if err := json.Unmarshal(data, &cf); err != nil {
  89  		return nil, err
  90  	}
  91  	return &cf, nil
  92  }
  93  
  94  // SaveConfigFile saves the configuration to the JSON file.
  95  func SaveConfigFile(cf *ConfigFile) error {
  96  	path := configFilePath()
  97  
  98  	// Ensure directory exists
  99  	dir := filepath.Dir(path)
 100  	if err := os.MkdirAll(dir, 0755); err != nil {
 101  		return err
 102  	}
 103  
 104  	data, err := json.MarshalIndent(cf, "", "  ")
 105  	if err != nil {
 106  		return err
 107  	}
 108  
 109  	return os.WriteFile(path, data, 0644)
 110  }
 111  
 112  // ConfigToFile converts a Config to a ConfigFile for persistence.
 113  func ConfigToFile(cfg *Config) *ConfigFile {
 114  	return &ConfigFile{
 115  		DBBackend:              cfg.DBBackend,
 116  		DBBinary:               cfg.DBBinary,
 117  		RelayBinary:            cfg.RelayBinary,
 118  		ACLBinary:              cfg.ACLBinary,
 119  		DBListen:               cfg.DBListen,
 120  		ACLListen:              cfg.ACLListen,
 121  		ACLEnabled:             &cfg.ACLEnabled,
 122  		ACLMode:                cfg.ACLMode,
 123  		DataDir:                cfg.DataDir,
 124  		LogLevel:               cfg.LogLevel,
 125  		AdminPort:              &cfg.AdminPort,
 126  		AdminOwners:            cfg.AdminOwners,
 127  		BinDir:                 cfg.BinDir,
 128  		DistributedSyncEnabled: &cfg.DistributedSyncEnabled,
 129  		ClusterSyncEnabled:     &cfg.ClusterSyncEnabled,
 130  		RelayGroupEnabled:      &cfg.RelayGroupEnabled,
 131  		NegentropyEnabled:      &cfg.NegentropyEnabled,
 132  		NegentropyBinary:       cfg.NegentropyBinary,
 133  		NegentropyListen:       cfg.NegentropyListen,
 134  		CertsEnabled:           &cfg.CertsEnabled,
 135  		CertsBinary:            cfg.CertsBinary,
 136  		NitsEnabled:            &cfg.NitsEnabled,
 137  		NitsBinary:             cfg.NitsBinary,
 138  		NitsShimBinary:         cfg.NitsShimBinary,
 139  		NitsListen:             cfg.NitsListen,
 140  		NitsRPCPort:            &cfg.NitsRPCPort,
 141  		NitsDataDir:            cfg.NitsDataDir,
 142  		NitsPruneMB:            &cfg.NitsPruneMB,
 143  		NitsNetwork:            cfg.NitsNetwork,
 144  		LukEnabled:             &cfg.LukEnabled,
 145  		LukBinary:              cfg.LukBinary,
 146  		LukDataDir:             cfg.LukDataDir,
 147  		LukRPCListen:           cfg.LukRPCListen,
 148  		LukPeerListen:          cfg.LukPeerListen,
 149  		StrelaEnabled:           &cfg.StrelaEnabled,
 150  		StrelaBinary:           cfg.StrelaBinary,
 151  		StrelaPort:              &cfg.StrelaPort,
 152  		StrelaDataDir:           cfg.StrelaDataDir,
 153  	}
 154  }
 155  
 156  // Config holds the launcher configuration.
 157  type Config struct {
 158  	// DBBackend is the database backend: badger or neo4j
 159  	DBBackend string
 160  
 161  	// DBBinary is the path to the database server binary (computed from DBBackend if not set)
 162  	DBBinary string
 163  
 164  	// RelayBinary is the path to the orly binary
 165  	RelayBinary string
 166  
 167  	// ACLBinary is the path to the ACL server binary (computed from ACLMode if not set)
 168  	ACLBinary string
 169  
 170  	// DBListen is the address the database server listens on
 171  	DBListen string
 172  
 173  	// ACLListen is the address the ACL server listens on
 174  	ACLListen string
 175  
 176  	// ACLEnabled controls whether to run the ACL server as a separate process
 177  	// When false, the relay runs in open mode (no ACL restrictions)
 178  	ACLEnabled bool
 179  
 180  	// ACLMode is the ACL mode: follows, managed, curation
 181  	// Determines which ACL binary to use when ACLEnabled is true
 182  	ACLMode string
 183  
 184  	// DBReadyTimeout is how long to wait for the database to be ready
 185  	DBReadyTimeout time.Duration
 186  
 187  	// ACLReadyTimeout is how long to wait for the ACL server to be ready
 188  	ACLReadyTimeout time.Duration
 189  
 190  	// StopTimeout is how long to wait for processes to stop gracefully
 191  	StopTimeout time.Duration
 192  
 193  	// DataDir is the data directory to pass to orly-db
 194  	DataDir string
 195  
 196  	// LogLevel is the log level to use for all processes
 197  	LogLevel string
 198  
 199  	// Sync service configuration
 200  	// DistributedSyncEnabled enables the distributed sync service
 201  	DistributedSyncEnabled bool
 202  	// DistributedSyncBinary is the path to the distributed sync binary
 203  	DistributedSyncBinary string
 204  	// DistributedSyncListen is the gRPC listen address for distributed sync
 205  	DistributedSyncListen string
 206  
 207  	// ClusterSyncEnabled enables the cluster sync service
 208  	ClusterSyncEnabled bool
 209  	// ClusterSyncBinary is the path to the cluster sync binary
 210  	ClusterSyncBinary string
 211  	// ClusterSyncListen is the gRPC listen address for cluster sync
 212  	ClusterSyncListen string
 213  
 214  	// RelayGroupEnabled enables the relay group service
 215  	RelayGroupEnabled bool
 216  	// RelayGroupBinary is the path to the relay group binary
 217  	RelayGroupBinary string
 218  	// RelayGroupListen is the gRPC listen address for relay group
 219  	RelayGroupListen string
 220  
 221  	// NegentropyEnabled enables the negentropy sync service
 222  	NegentropyEnabled bool
 223  	// NegentropyBinary is the path to the negentropy sync binary
 224  	NegentropyBinary string
 225  	// NegentropyListen is the gRPC listen address for negentropy
 226  	NegentropyListen string
 227  
 228  	// SyncReadyTimeout is how long to wait for sync services to be ready
 229  	SyncReadyTimeout time.Duration
 230  
 231  	// Certificate service configuration
 232  	// CertsEnabled enables the certificate service
 233  	CertsEnabled bool
 234  	// CertsBinary is the path to the certificate service binary
 235  	CertsBinary string
 236  
 237  	// Bitcoin node (nits) configuration
 238  	// NitsEnabled enables the Bitcoin node manager
 239  	NitsEnabled bool
 240  	// NitsBinary is the path to the bitcoind binary
 241  	NitsBinary string
 242  	// NitsShimBinary is the path to the orly-nits gRPC shim binary
 243  	NitsShimBinary string
 244  	// NitsListen is the gRPC listen address for the nits shim
 245  	NitsListen string
 246  	// NitsRPCPort is the JSON-RPC port for bitcoind
 247  	NitsRPCPort int
 248  	// NitsDataDir is the data directory for bitcoind
 249  	NitsDataDir string
 250  	// NitsPruneMB is the prune target in MB (0 = no pruning)
 251  	NitsPruneMB int
 252  	// NitsNetwork is the bitcoin network: mainnet, testnet, signet, regtest
 253  	NitsNetwork string
 254  	// NitsReadyTimeout is how long to wait for bitcoind to respond
 255  	NitsReadyTimeout time.Duration
 256  
 257  	// Lightning node (luk) configuration
 258  	// LukEnabled enables the Lightning node
 259  	LukEnabled bool
 260  	// LukBinary is the path to the luk binary
 261  	LukBinary string
 262  	// LukDataDir is the data directory for luk
 263  	LukDataDir string
 264  	// LukRPCListen is the gRPC listen address for luk
 265  	LukRPCListen string
 266  	// LukPeerListen is the P2P listen address for luk
 267  	LukPeerListen string
 268  	// LukReadyTimeout is how long to wait for luk gRPC to be reachable
 269  	LukReadyTimeout time.Duration
 270  
 271  	// Wallet (strela) configuration
 272  	// StrelaEnabled enables the wallet web UI
 273  	StrelaEnabled bool
 274  	// StrelaBinary is the path to the strela binary
 275  	StrelaBinary string
 276  	// StrelaPort is the HTTP port for strela
 277  	StrelaPort int
 278  	// StrelaDataDir is the work directory for strela
 279  	StrelaDataDir string
 280  	// StrelaReadyTimeout is how long to wait for strela HTTP to be reachable
 281  	StrelaReadyTimeout time.Duration
 282  
 283  	// ServicesEnabled controls whether to start the DB, relay, and other services
 284  	// When false, only the admin UI runs (useful for initial setup/updates)
 285  	ServicesEnabled bool
 286  
 287  	// Admin UI configuration
 288  	// AdminEnabled controls whether to run the admin HTTP server
 289  	AdminEnabled bool
 290  	// AdminPort is the port for the admin HTTP server
 291  	AdminPort int
 292  	// AdminOwners is a list of pubkeys (hex) allowed to access the admin UI
 293  	AdminOwners []string
 294  	// BinDir is the directory for versioned binary management
 295  	BinDir string
 296  }
 297  
 298  func loadConfig() (*Config, error) {
 299  	// Load config file first (provides defaults)
 300  	cf, err := loadConfigFile()
 301  	if err != nil {
 302  		// Log but don't fail - env vars are still valid
 303  		cf = &ConfigFile{}
 304  	}
 305  
 306  	// Get backend and mode - file first, then env
 307  	dbBackend := stringOr(cf.DBBackend, getEnvOrDefault("ORLY_LAUNCHER_DB_BACKEND", "badger"))
 308  	aclMode := stringOr(cf.ACLMode, getEnvOrDefault("ORLY_ACL_MODE", "follows"))
 309  
 310  	// Compute default binary names based on backend/mode
 311  	defaultDBBinary := "orly-db-" + dbBackend
 312  	defaultACLBinary := "orly-acl-" + aclMode
 313  
 314  	// Parse admin owners - env takes precedence, then file
 315  	envOwners := getEnvOrDefault("ORLY_LAUNCHER_OWNERS", "")
 316  	var adminOwners []string
 317  	if envOwners != "" {
 318  		adminOwners = parseOwnersList(envOwners)
 319  	} else if len(cf.AdminOwners) > 0 {
 320  		adminOwners = cf.AdminOwners
 321  	}
 322  
 323  	cfg := &Config{
 324  		DBBackend:       dbBackend,
 325  		DBBinary:        envOrFileOrDefault("ORLY_LAUNCHER_DB_BINARY", cf.DBBinary, defaultDBBinary),
 326  		RelayBinary:     envOrFileOrDefault("ORLY_LAUNCHER_RELAY_BINARY", cf.RelayBinary, "orly"),
 327  		ACLBinary:       envOrFileOrDefault("ORLY_LAUNCHER_ACL_BINARY", cf.ACLBinary, defaultACLBinary),
 328  		DBListen:        envOrFileOrDefault("ORLY_LAUNCHER_DB_LISTEN", cf.DBListen, "127.0.0.1:50051"),
 329  		ACLListen:       envOrFileOrDefault("ORLY_LAUNCHER_ACL_LISTEN", cf.ACLListen, "127.0.0.1:50052"),
 330  		ACLEnabled:      boolEnvOrFile("ORLY_LAUNCHER_ACL_ENABLED", cf.ACLEnabled, false),
 331  		ACLMode:         aclMode,
 332  		DBReadyTimeout:  parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second),
 333  		ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 120*time.Second),
 334  		StopTimeout:     parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second),
 335  		DataDir:         envOrFileOrDefault("ORLY_DATA_DIR", cf.DataDir, filepath.Join(xdg.DataHome, "ORLY")),
 336  		LogLevel:        envOrFileOrDefault("ORLY_LOG_LEVEL", cf.LogLevel, "info"),
 337  
 338  		// Sync services configuration
 339  		DistributedSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", cf.DistributedSyncEnabled, false),
 340  		DistributedSyncBinary:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"),
 341  		DistributedSyncListen:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"),
 342  
 343  		ClusterSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", cf.ClusterSyncEnabled, false),
 344  		ClusterSyncBinary:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"),
 345  		ClusterSyncListen:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"),
 346  
 347  		RelayGroupEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", cf.RelayGroupEnabled, false),
 348  		RelayGroupBinary:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"),
 349  		RelayGroupListen:  getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"),
 350  
 351  		NegentropyEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", cf.NegentropyEnabled, false),
 352  		NegentropyBinary:  envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", cf.NegentropyBinary, "orly-sync-negentropy"),
 353  		NegentropyListen:  envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", cf.NegentropyListen, "127.0.0.1:50064"),
 354  
 355  		SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second),
 356  
 357  		// Certificate service configuration
 358  		CertsEnabled: boolEnvOrFile("ORLY_LAUNCHER_CERTS_ENABLED", cf.CertsEnabled, false),
 359  		CertsBinary:  envOrFileOrDefault("ORLY_LAUNCHER_CERTS_BINARY", cf.CertsBinary, "orly-certs"),
 360  
 361  		// Bitcoin node (nits) configuration
 362  		NitsEnabled:      boolEnvOrFile("ORLY_LAUNCHER_NITS_ENABLED", cf.NitsEnabled, false),
 363  		NitsBinary:       envOrFileOrDefault("ORLY_LAUNCHER_NITS_BINARY", cf.NitsBinary, "bitcoind"),
 364  		NitsShimBinary:   envOrFileOrDefault("ORLY_LAUNCHER_NITS_SHIM_BINARY", cf.NitsShimBinary, "orly-nits"),
 365  		NitsListen:       envOrFileOrDefault("ORLY_LAUNCHER_NITS_LISTEN", cf.NitsListen, "127.0.0.1:50070"),
 366  		NitsRPCPort:      intEnvOrFile("ORLY_LAUNCHER_NITS_RPC_PORT", cf.NitsRPCPort, 8332),
 367  		NitsDataDir:      envOrFileOrDefault("ORLY_LAUNCHER_NITS_DATA_DIR", cf.NitsDataDir, filepath.Join(xdg.DataHome, "orly", "nits")),
 368  		NitsPruneMB:      intEnvOrFile("ORLY_LAUNCHER_NITS_PRUNE_MB", cf.NitsPruneMB, 2048),
 369  		NitsNetwork:      envOrFileOrDefault("ORLY_LAUNCHER_NITS_NETWORK", cf.NitsNetwork, "mainnet"),
 370  		NitsReadyTimeout: parseDuration("ORLY_LAUNCHER_NITS_READY_TIMEOUT", 120*time.Second),
 371  
 372  		// Lightning node (luk) configuration
 373  		LukEnabled:      boolEnvOrFile("ORLY_LAUNCHER_LUK_ENABLED", cf.LukEnabled, false),
 374  		LukBinary:       envOrFileOrDefault("ORLY_LAUNCHER_LUK_BINARY", cf.LukBinary, "luk"),
 375  		LukDataDir:      envOrFileOrDefault("ORLY_LAUNCHER_LUK_DATA_DIR", cf.LukDataDir, filepath.Join(xdg.DataHome, "orly", "luk")),
 376  		LukRPCListen:    envOrFileOrDefault("ORLY_LAUNCHER_LUK_RPC_LISTEN", cf.LukRPCListen, "127.0.0.1:10009"),
 377  		LukPeerListen:   envOrFileOrDefault("ORLY_LAUNCHER_LUK_PEER_LISTEN", cf.LukPeerListen, "0.0.0.0:9735"),
 378  		LukReadyTimeout: parseDuration("ORLY_LAUNCHER_LUK_READY_TIMEOUT", 60*time.Second),
 379  
 380  		// Wallet (strela) configuration
 381  		StrelaEnabled:      boolEnvOrFile("ORLY_LAUNCHER_STRELA_ENABLED", cf.StrelaEnabled, false),
 382  		StrelaBinary:      envOrFileOrDefault("ORLY_LAUNCHER_STRELA_BINARY", cf.StrelaBinary, "strela"),
 383  		StrelaPort:         intEnvOrFile("ORLY_LAUNCHER_STRELA_PORT", cf.StrelaPort, 8090),
 384  		StrelaDataDir:      envOrFileOrDefault("ORLY_LAUNCHER_STRELA_DATA_DIR", cf.StrelaDataDir, filepath.Join(xdg.DataHome, "orly", "strela")),
 385  		StrelaReadyTimeout: parseDuration("ORLY_LAUNCHER_STRELA_READY_TIMEOUT", 30*time.Second),
 386  
 387  		// Services enabled (default true for backwards compatibility)
 388  		ServicesEnabled: getEnvOrDefault("ORLY_LAUNCHER_SERVICES_ENABLED", "true") == "true",
 389  
 390  		// Admin UI configuration
 391  		AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true",
 392  		AdminPort:    intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080),
 393  		AdminOwners:  adminOwners,
 394  		BinDir:       envOrFileOrDefault("ORLY_LAUNCHER_BIN_DIR", cf.BinDir, filepath.Join(xdg.DataHome, "orly", "bin")),
 395  	}
 396  
 397  	return cfg, nil
 398  }
 399  
 400  // stringOr returns the first non-empty string.
 401  func stringOr(a, b string) string {
 402  	if a != "" {
 403  		return a
 404  	}
 405  	return b
 406  }
 407  
 408  // envOrFileOrDefault returns env var if set, then file value if set, then default.
 409  func envOrFileOrDefault(envKey, fileValue, defaultValue string) string {
 410  	if v := os.Getenv(envKey); v != "" {
 411  		return v
 412  	}
 413  	if fileValue != "" {
 414  		return fileValue
 415  	}
 416  	return defaultValue
 417  }
 418  
 419  // boolEnvOrFile returns env var if set, then file value if set, then default.
 420  func boolEnvOrFile(envKey string, fileValue *bool, defaultValue bool) bool {
 421  	if v := os.Getenv(envKey); v != "" {
 422  		return v == "true"
 423  	}
 424  	if fileValue != nil {
 425  		return *fileValue
 426  	}
 427  	return defaultValue
 428  }
 429  
 430  // intEnvOrFile returns env var if set, then file value if set, then default.
 431  func intEnvOrFile(envKey string, fileValue *int, defaultValue int) int {
 432  	if v := os.Getenv(envKey); v != "" {
 433  		if i, err := strconv.Atoi(v); err == nil {
 434  			return i
 435  		}
 436  	}
 437  	if fileValue != nil {
 438  		return *fileValue
 439  	}
 440  	return defaultValue
 441  }
 442  
 443  func parseOwnersList(s string) []string {
 444  	if s == "" {
 445  		return nil
 446  	}
 447  	parts := strings.Split(s, ",")
 448  	var owners []string
 449  	for _, p := range parts {
 450  		p = strings.TrimSpace(p)
 451  		if p != "" {
 452  			owners = append(owners, p)
 453  		}
 454  	}
 455  	return owners
 456  }
 457  
 458  func parseInt(key string, defaultValue int) int {
 459  	if v := os.Getenv(key); v != "" {
 460  		if i, err := strconv.Atoi(v); err == nil {
 461  			return i
 462  		}
 463  	}
 464  	return defaultValue
 465  }
 466  
 467  func getEnvOrDefault(key, defaultValue string) string {
 468  	if v := os.Getenv(key); v != "" {
 469  		return v
 470  	}
 471  	return defaultValue
 472  }
 473  
 474  func parseDuration(key string, defaultValue time.Duration) time.Duration {
 475  	if v := os.Getenv(key); v != "" {
 476  		if d, err := time.ParseDuration(v); err == nil {
 477  			return d
 478  		}
 479  	}
 480  	return defaultValue
 481  }
 482