// Package config holds application configuration loaded from environment variables. package config import ( "fmt" "os" "time" ) // Config holds all application-level configuration. type Config struct { // DSN is the MySQL connection string. // Format: user:password@tcp(host:port)/dbname?parseTime=true&loc=UTC&time_zone=%%27UTC%%27 DSN string // RedisAddr is the Redis server address (host:port). RedisAddr string RedisPassword string RedisDB int // WebhookSecret is the HMAC-SHA256 shared secret for the card-store webhook. // Must be set; no default. WebhookSecret string // RedeemFailMax is the number of consecutive redeem failures before a 1-hour lock. // Default: 5 RedeemFailMax int // RedeemLockDuration is how long the lock lasts after hitting RedeemFailMax. // Default: 1 hour RedeemLockDuration time.Duration // WebhookTimestampTolerance is the ±window for webhook timestamp validation. // Default: 5 minutes WebhookTimestampTolerance time.Duration // WebhookNonceTTL is how long a webhook nonce is kept in Redis to prevent replay. // Should be > 2 * WebhookTimestampTolerance. Default: 15 minutes. WebhookNonceTTL time.Duration } // FromEnv reads configuration from environment variables. // Returns an error if required variables are missing. func FromEnv() (*Config, error) { c := &Config{ DSN: os.Getenv("DB_DSN"), RedisAddr: getEnvDefault("REDIS_ADDR", "127.0.0.1:6379"), RedisPassword: os.Getenv("REDIS_PASSWORD"), RedisDB: 0, WebhookSecret: os.Getenv("WEBHOOK_SECRET"), RedeemFailMax: 5, RedeemLockDuration: time.Hour, WebhookTimestampTolerance: 5 * time.Minute, WebhookNonceTTL: 15 * time.Minute, } if c.DSN == "" { return nil, fmt.Errorf("config: DB_DSN is required") } if c.WebhookSecret == "" { return nil, fmt.Errorf("config: WEBHOOK_SECRET is required") } return c, nil } func getEnvDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }