// Package config holds application configuration loaded from environment variables. package config import ( "fmt" "os" "strings" "time" ) // Config holds all application-level configuration. type Config struct { // Driver selects the database engine: "mysql" (default) or "sqlite". // Read from DB_DRIVER. Switching engines is a single env var change. Driver string // DSN is the database connection string. // mysql: user:password@tcp(host:port)/dbname?parseTime=true&loc=UTC&time_zone=%%27UTC%%27 // sqlite: a file path (e.g. /var/lib/pangolin/pangolin.db) or :memory: 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 // ── Auth / JWT (RS256) ──────────────────────────────────────────────── // JWTPrivateKeyPath is the PEM file holding the active RS256 signing key. JWTPrivateKeyPath string // JWTKeyID is the `kid` written into the JWT header (identifies the signing key). JWTKeyID string // JWTPublicKeys maps kid -> PEM public-key file path. It must include the // active key's kid and may carry previous keys still accepted during // rotation. Parsed from JWT_PUBLIC_KEYS="kid1:/path1,kid2:/path2". JWTPublicKeys map[string]string // ─── gRPC / agent server ────────────────────────────────────────────── // GRPCAddr is the listen address for the mTLS gRPC agent server (host:port). // Optional: if empty, the gRPC server is not started. // Example: ":9443" GRPCAddr string // CAKeyPath and CACertPath are file paths for the Pangolin Node CA. // Required when GRPCAddr is set. CAKeyPath string CACertPath string // GRPCCertPath and GRPCKeyPath are the TLS certificate/key used by the gRPC // listener for its own transport credential (distinct from the node CA). // Required when GRPCAddr is set. GRPCCertPath string GRPCKeyPath string } // FromEnv reads configuration from environment variables. // Returns an error if required variables are missing. func FromEnv() (*Config, error) { c := &Config{ Driver: os.Getenv("DB_DRIVER"), 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, JWTPrivateKeyPath: os.Getenv("JWT_PRIVATE_KEY_PATH"), JWTKeyID: os.Getenv("JWT_KEY_ID"), JWTPublicKeys: parseKeyMap(os.Getenv("JWT_PUBLIC_KEYS")), // gRPC server (optional) GRPCAddr: os.Getenv("GRPC_ADDR"), CAKeyPath: os.Getenv("CA_KEY_PATH"), CACertPath: os.Getenv("CA_CERT_PATH"), GRPCCertPath: os.Getenv("GRPC_CERT_PATH"), GRPCKeyPath: os.Getenv("GRPC_KEY_PATH"), } 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 } // parseKeyMap parses a "kid1:/path1,kid2:/path2" string into a map. Empty input // yields a nil map. Malformed entries (missing ':') are skipped. func parseKeyMap(raw string) map[string]string { if raw == "" { return nil } m := map[string]string{} for _, pair := range strings.Split(raw, ",") { pair = strings.TrimSpace(pair) if pair == "" { continue } i := strings.IndexByte(pair, ':') if i <= 0 || i == len(pair)-1 { continue } m[strings.TrimSpace(pair[:i])] = strings.TrimSpace(pair[i+1:]) } return m }