package config import ( "fmt" "os" "strconv" "strings" "github.com/joho/godotenv" ) type LightningConfig struct { Enabled bool LNbitsURL string LNbitsInvoiceKey string PriceYearlySats int64 PriceLifetimeSats int64 InvoiceExpiryMins int InvoiceMemoYearly string InvoiceMemoLifetime string } type NostrConfig struct { Relays []string UsernameSyncEnabled bool SyncIntervalMins int } type DMConfig struct { Enabled bool Nsec string Kind int MessagesFile string } type ExpiryConfig struct { ReminderDays []int GraceDays int CronHourUTC int } type WebhookConfig struct { URL string Secret string TimeoutSecs int MaxRetries int } type Config struct { Domain string Port int AdminAPIKey string FrontendURL string DatabasePath string Lightning LightningConfig Nostr NostrConfig DM DMConfig Expiry ExpiryConfig Webhook WebhookConfig LogLevel string RateLimitPerMin int ReservedUsernames []string // CORS: exact origin list = FRONTEND_URL ∪ CORS_ORIGINS; loopback hosts if CORS_ALLOW_LOCALHOST. CORSExtraOrigins []string CORSAllowLocalhost bool CORSAllowCredentials bool } func Load() (*Config, error) { _ = godotenv.Load() c := &Config{ Domain: env("DOMAIN", ""), Port: envInt("PORT", 8080), AdminAPIKey: env("ADMIN_API_KEY", ""), FrontendURL: env("FRONTEND_URL", ""), DatabasePath: env("DATABASE_PATH", ".data/nip05.db"), Lightning: LightningConfig{ Enabled: envBool("LIGHTNING_ENABLED", true), LNbitsURL: env("LNBITS_URL", ""), LNbitsInvoiceKey: env("LNBITS_INVOICE_KEY", ""), PriceYearlySats: int64(envInt("PRICE_YEARLY_SATS", 1000)), PriceLifetimeSats: int64(envInt("PRICE_LIFETIME_SATS", 10000)), InvoiceExpiryMins: envInt("INVOICE_EXPIRY_MINUTES", 30), InvoiceMemoYearly: env("INVOICE_MEMO_YEARLY", "Noderunners Relay yearly Access"), InvoiceMemoLifetime: env("INVOICE_MEMO_LIFETIME", "Noderunners Relay lifetime Access"), }, Nostr: NostrConfig{ Relays: csv(env("RELAYS", "")), UsernameSyncEnabled: envBool("USERNAME_SYNC_ENABLED", true), SyncIntervalMins: envInt("SYNC_INTERVAL_MINUTES", 15), }, DM: DMConfig{ Enabled: envBool("DM_ENABLED", true), Nsec: env("DM_NSEC", ""), Kind: envInt("DM_KIND", 1059), MessagesFile: env("MESSAGES_FILE", "messages.yaml"), }, Expiry: ExpiryConfig{ ReminderDays: csvInt(env("EXPIRY_REMINDER_DAYS", "7")), GraceDays: envInt("USERNAME_GRACE_DAYS", 30), CronHourUTC: envInt("EXPIRY_CRON_HOUR_UTC", 9), }, Webhook: WebhookConfig{ URL: env("WEBHOOK_URL", ""), Secret: env("WEBHOOK_SECRET", ""), TimeoutSecs: envInt("WEBHOOK_TIMEOUT_SECONDS", 10), MaxRetries: envInt("WEBHOOK_MAX_RETRIES", 5), }, LogLevel: env("LOG_LEVEL", "info"), RateLimitPerMin: envInt("RATE_LIMIT_PER_MIN", 30), ReservedUsernames: csv(env("RESERVED_USERNAMES", "")), CORSExtraOrigins: csv(env("CORS_ORIGINS", "")), CORSAllowLocalhost: envBool("CORS_ALLOW_LOCALHOST", true), CORSAllowCredentials: envBool("CORS_ALLOW_CREDENTIALS", false), } if err := Validate(c); err != nil { return nil, err } return c, nil } func env(key, def string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return def } func envInt(key string, def int) int { if v, ok := os.LookupEnv(key); ok && v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func envBool(key string, def bool) bool { if v, ok := os.LookupEnv(key); ok && v != "" { switch strings.ToLower(v) { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false } } return def } func csv(v string) []string { if v == "" { return nil } parts := strings.Split(v, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } func csvInt(v string) []int { parts := csv(v) out := make([]int, 0, len(parts)) for _, p := range parts { n, err := strconv.Atoi(p) if err != nil { continue } out = append(out, n) } return out } func (c *Config) Addr() string { return fmt.Sprintf(":%d", c.Port) } // CORSExactOrigins lists allowed browser Origins for exact match (before loopback wildcard). func (c *Config) CORSExactOrigins() []string { seen := make(map[string]bool) out := make([]string, 0, 4+len(c.CORSExtraOrigins)) add := func(s string) { s = strings.TrimSpace(s) if s == "" || seen[s] { return } seen[s] = true out = append(out, s) } add(c.FrontendURL) for _, o := range c.CORSExtraOrigins { add(o) } return out }