summaryrefslogtreecommitdiffstats
path: root/internal/config/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go53
1 files changed, 6 insertions, 47 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index 294510d..fc392b4 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -10,7 +10,6 @@ import (
10 "gopkg.in/yaml.v3" 10 "gopkg.in/yaml.v3"
11) 11)
12 12
13// Config holds all configuration for the relay.
14type Config struct { 13type Config struct {
15 Server ServerConfig `yaml:"server"` 14 Server ServerConfig `yaml:"server"`
16 Database DatabaseConfig `yaml:"database"` 15 Database DatabaseConfig `yaml:"database"`
@@ -21,7 +20,6 @@ type Config struct {
21 Storage StorageConfig `yaml:"storage"` 20 Storage StorageConfig `yaml:"storage"`
22} 21}
23 22
24// ServerConfig holds server configuration.
25type ServerConfig struct { 23type ServerConfig struct {
26 GrpcAddr string `yaml:"grpc_addr"` 24 GrpcAddr string `yaml:"grpc_addr"`
27 HttpAddr string `yaml:"http_addr"` 25 HttpAddr string `yaml:"http_addr"`
@@ -30,14 +28,10 @@ type ServerConfig struct {
30 WriteTimeout time.Duration `yaml:"write_timeout"` 28 WriteTimeout time.Duration `yaml:"write_timeout"`
31} 29}
32 30
33// DatabaseConfig holds database configuration.
34type DatabaseConfig struct { 31type DatabaseConfig struct {
35 Path string `yaml:"path"` 32 Path string `yaml:"path"`
36 // Note: SQLite connection pooling is handled internally in the storage layer.
37 // SQLite works best with a single connection due to its single-writer architecture.
38} 33}
39 34
40// AuthConfig holds authentication configuration.
41type AuthConfig struct { 35type AuthConfig struct {
42 Read AuthOperationConfig `yaml:"read"` 36 Read AuthOperationConfig `yaml:"read"`
43 Write AuthOperationConfig `yaml:"write"` 37 Write AuthOperationConfig `yaml:"write"`
@@ -45,16 +39,14 @@ type AuthConfig struct {
45 SkipMethods []string `yaml:"skip_methods"` 39 SkipMethods []string `yaml:"skip_methods"`
46} 40}
47 41
48// AuthOperationConfig configures auth for a specific operation type (read or write). 42// AuthOperationConfig configures auth for read or write operations.
43// Three states: disabled (allow all), enabled with empty list (require auth),
44// enabled with npubs (whitelist only). Npubs normalized to hex at load time.
49type AuthOperationConfig struct { 45type AuthOperationConfig struct {
50 Enabled bool `yaml:"enabled"` // false = no auth required, true = auth required 46 Enabled bool `yaml:"enabled"`
51 AllowedNpubs []string `yaml:"allowed_npubs"` // npub format only - normalized to hex internally 47 AllowedNpubs []string `yaml:"allowed_npubs"`
52 // If enabled=false: no auth, allow all
53 // If enabled=true && allowed_npubs=[]: auth required, any valid signature accepted
54 // If enabled=true && allowed_npubs=[...]: auth required, only whitelisted npubs
55} 48}
56 49
57// RateLimitConfig holds rate limiting configuration.
58type RateLimitConfig struct { 50type RateLimitConfig struct {
59 Enabled bool `yaml:"enabled"` 51 Enabled bool `yaml:"enabled"`
60 DefaultRPS float64 `yaml:"default_rps"` 52 DefaultRPS float64 `yaml:"default_rps"`
@@ -69,20 +61,17 @@ type RateLimitConfig struct {
69 MaxIdleTime time.Duration `yaml:"max_idle_time"` 61 MaxIdleTime time.Duration `yaml:"max_idle_time"`
70} 62}
71 63
72// MethodLimit defines rate limits for a specific method.
73type MethodLimit struct { 64type MethodLimit struct {
74 RPS float64 `yaml:"rps"` 65 RPS float64 `yaml:"rps"`
75 Burst int `yaml:"burst"` 66 Burst int `yaml:"burst"`
76} 67}
77 68
78// UserLimit defines rate limits for a specific user.
79type UserLimit struct { 69type UserLimit struct {
80 RPS float64 `yaml:"rps"` 70 RPS float64 `yaml:"rps"`
81 Burst int `yaml:"burst"` 71 Burst int `yaml:"burst"`
82 Methods map[string]MethodLimit `yaml:"methods"` 72 Methods map[string]MethodLimit `yaml:"methods"`
83} 73}
84 74
85// MetricsConfig holds metrics configuration.
86type MetricsConfig struct { 75type MetricsConfig struct {
87 Enabled bool `yaml:"enabled"` 76 Enabled bool `yaml:"enabled"`
88 Addr string `yaml:"addr"` 77 Addr string `yaml:"addr"`
@@ -91,14 +80,12 @@ type MetricsConfig struct {
91 Subsystem string `yaml:"subsystem"` 80 Subsystem string `yaml:"subsystem"`
92} 81}
93 82
94// LoggingConfig holds logging configuration.
95type LoggingConfig struct { 83type LoggingConfig struct {
96 Level string `yaml:"level"` 84 Level string `yaml:"level"`
97 Format string `yaml:"format"` 85 Format string `yaml:"format"`
98 Output string `yaml:"output"` 86 Output string `yaml:"output"`
99} 87}
100 88
101// StorageConfig holds storage configuration.
102type StorageConfig struct { 89type StorageConfig struct {
103 AutoCompact bool `yaml:"auto_compact"` 90 AutoCompact bool `yaml:"auto_compact"`
104 CompactInterval time.Duration `yaml:"compact_interval"` 91 CompactInterval time.Duration `yaml:"compact_interval"`
@@ -157,12 +144,9 @@ func Default() *Config {
157 } 144 }
158} 145}
159 146
160// Load loads configuration from a YAML file and applies environment variable overrides.
161func Load(filename string) (*Config, error) { 147func Load(filename string) (*Config, error) {
162 // Start with defaults
163 cfg := Default() 148 cfg := Default()
164 149
165 // Read file if provided
166 if filename != "" { 150 if filename != "" {
167 data, err := os.ReadFile(filename) 151 data, err := os.ReadFile(filename)
168 if err != nil { 152 if err != nil {
@@ -174,15 +158,12 @@ func Load(filename string) (*Config, error) {
174 } 158 }
175 } 159 }
176 160
177 // Apply environment variable overrides
178 applyEnvOverrides(cfg) 161 applyEnvOverrides(cfg)
179 162
180 // Normalize npubs to hex pubkeys
181 if err := normalizeNpubs(cfg); err != nil { 163 if err := normalizeNpubs(cfg); err != nil {
182 return nil, fmt.Errorf("failed to normalize npubs: %w", err) 164 return nil, fmt.Errorf("failed to normalize npubs: %w", err)
183 } 165 }
184 166
185 // Validate
186 if err := cfg.Validate(); err != nil { 167 if err := cfg.Validate(); err != nil {
187 return nil, fmt.Errorf("invalid configuration: %w", err) 168 return nil, fmt.Errorf("invalid configuration: %w", err)
188 } 169 }
@@ -190,19 +171,15 @@ func Load(filename string) (*Config, error) {
190 return cfg, nil 171 return cfg, nil
191} 172}
192 173
193// normalizeNpubs converts all npub (bech32) pubkeys to hex format. 174// normalizeNpubs converts npub (bech32) to hex format for internal use.
194// Config only accepts npub format (human-readable), which is converted
195// to hex format (computer-readable) for internal use.
196func normalizeNpubs(cfg *Config) error { 175func normalizeNpubs(cfg *Config) error {
197 var err error 176 var err error
198 177
199 // Normalize read allowlist
200 cfg.Auth.Read.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Read.AllowedNpubs) 178 cfg.Auth.Read.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Read.AllowedNpubs)
201 if err != nil { 179 if err != nil {
202 return fmt.Errorf("auth.read.allowed_npubs: %w", err) 180 return fmt.Errorf("auth.read.allowed_npubs: %w", err)
203 } 181 }
204 182
205 // Normalize write allowlist
206 cfg.Auth.Write.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Write.AllowedNpubs) 183 cfg.Auth.Write.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Write.AllowedNpubs)
207 if err != nil { 184 if err != nil {
208 return fmt.Errorf("auth.write.allowed_npubs: %w", err) 185 return fmt.Errorf("auth.write.allowed_npubs: %w", err)
@@ -211,7 +188,6 @@ func normalizeNpubs(cfg *Config) error {
211 return nil 188 return nil
212} 189}
213 190
214// normalizeNpubList converts a list of npubs to hex pubkeys.
215func normalizeNpubList(npubs []string) ([]string, error) { 191func normalizeNpubList(npubs []string) ([]string, error) {
216 if len(npubs) == 0 { 192 if len(npubs) == 0 {
217 return nil, nil 193 return nil, nil
@@ -219,33 +195,27 @@ func normalizeNpubList(npubs []string) ([]string, error) {
219 195
220 normalized := make([]string, 0, len(npubs)) 196 normalized := make([]string, 0, len(npubs))
221 for _, npub := range npubs { 197 for _, npub := range npubs {
222 // Skip empty strings
223 npub = strings.TrimSpace(npub) 198 npub = strings.TrimSpace(npub)
224 if npub == "" { 199 if npub == "" {
225 continue 200 continue
226 } 201 }
227 202
228 // Validate npub format
229 if !strings.HasPrefix(npub, "npub1") { 203 if !strings.HasPrefix(npub, "npub1") {
230 return nil, fmt.Errorf("invalid npub format %q: must start with 'npub1'", npub) 204 return nil, fmt.Errorf("invalid npub format %q: must start with 'npub1'", npub)
231 } 205 }
232 206
233 // Parse npub to get hex pubkey
234 key, err := nostr.ParsePublicKey(npub) 207 key, err := nostr.ParsePublicKey(npub)
235 if err != nil { 208 if err != nil {
236 return nil, fmt.Errorf("invalid npub %q: %w", npub, err) 209 return nil, fmt.Errorf("invalid npub %q: %w", npub, err)
237 } 210 }
238 211
239 // Get the hex representation for internal use
240 normalized = append(normalized, key.Public()) 212 normalized = append(normalized, key.Public())
241 } 213 }
242 214
243 return normalized, nil 215 return normalized, nil
244} 216}
245 217
246// Validate validates the configuration.
247func (c *Config) Validate() error { 218func (c *Config) Validate() error {
248 // Validate server addresses
249 if c.Server.GrpcAddr == "" { 219 if c.Server.GrpcAddr == "" {
250 return fmt.Errorf("server.grpc_addr is required") 220 return fmt.Errorf("server.grpc_addr is required")
251 } 221 }
@@ -253,12 +223,10 @@ func (c *Config) Validate() error {
253 return fmt.Errorf("server.http_addr is required") 223 return fmt.Errorf("server.http_addr is required")
254 } 224 }
255 225
256 // Validate database path
257 if c.Database.Path == "" { 226 if c.Database.Path == "" {
258 return fmt.Errorf("database.path is required") 227 return fmt.Errorf("database.path is required")
259 } 228 }
260 229
261 // Validate metrics config if enabled
262 if c.Metrics.Enabled { 230 if c.Metrics.Enabled {
263 if c.Metrics.Addr == "" { 231 if c.Metrics.Addr == "" {
264 return fmt.Errorf("metrics.addr is required when metrics enabled") 232 return fmt.Errorf("metrics.addr is required when metrics enabled")
@@ -268,7 +236,6 @@ func (c *Config) Validate() error {
268 } 236 }
269 } 237 }
270 238
271 // Validate logging
272 validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true} 239 validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
273 if !validLevels[c.Logging.Level] { 240 if !validLevels[c.Logging.Level] {
274 return fmt.Errorf("invalid logging.level: %s (must be debug, info, warn, or error)", c.Logging.Level) 241 return fmt.Errorf("invalid logging.level: %s (must be debug, info, warn, or error)", c.Logging.Level)
@@ -282,10 +249,7 @@ func (c *Config) Validate() error {
282 return nil 249 return nil
283} 250}
284 251
285// applyEnvOverrides applies environment variable overrides to the configuration.
286// Environment variables follow the pattern: MUXSTR_<SECTION>_<KEY>
287func applyEnvOverrides(cfg *Config) { 252func applyEnvOverrides(cfg *Config) {
288 // Server
289 if val := os.Getenv("MUXSTR_SERVER_GRPC_ADDR"); val != "" { 253 if val := os.Getenv("MUXSTR_SERVER_GRPC_ADDR"); val != "" {
290 cfg.Server.GrpcAddr = val 254 cfg.Server.GrpcAddr = val
291 } 255 }
@@ -306,12 +270,10 @@ func applyEnvOverrides(cfg *Config) {
306 } 270 }
307 } 271 }
308 272
309 // Database
310 if val := os.Getenv("MUXSTR_DATABASE_PATH"); val != "" { 273 if val := os.Getenv("MUXSTR_DATABASE_PATH"); val != "" {
311 cfg.Database.Path = val 274 cfg.Database.Path = val
312 } 275 }
313 276
314 // Auth
315 if val := os.Getenv("MUXSTR_AUTH_READ_ENABLED"); val != "" { 277 if val := os.Getenv("MUXSTR_AUTH_READ_ENABLED"); val != "" {
316 cfg.Auth.Read.Enabled = parseBool(val) 278 cfg.Auth.Read.Enabled = parseBool(val)
317 } 279 }
@@ -331,7 +293,6 @@ func applyEnvOverrides(cfg *Config) {
331 } 293 }
332 } 294 }
333 295
334 // Rate limit
335 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" { 296 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" {
336 cfg.RateLimit.Enabled = parseBool(val) 297 cfg.RateLimit.Enabled = parseBool(val)
337 } 298 }
@@ -348,7 +309,6 @@ func applyEnvOverrides(cfg *Config) {
348 } 309 }
349 } 310 }
350 311
351 // Metrics
352 if val := os.Getenv("MUXSTR_METRICS_ENABLED"); val != "" { 312 if val := os.Getenv("MUXSTR_METRICS_ENABLED"); val != "" {
353 cfg.Metrics.Enabled = parseBool(val) 313 cfg.Metrics.Enabled = parseBool(val)
354 } 314 }
@@ -359,7 +319,6 @@ func applyEnvOverrides(cfg *Config) {
359 cfg.Metrics.Path = val 319 cfg.Metrics.Path = val
360 } 320 }
361 321
362 // Logging
363 if val := os.Getenv("MUXSTR_LOGGING_LEVEL"); val != "" { 322 if val := os.Getenv("MUXSTR_LOGGING_LEVEL"); val != "" {
364 cfg.Logging.Level = val 323 cfg.Logging.Level = val
365 } 324 }