config.mx raw
1 package config
2
3 import (
4 "git.smesh.lol/moxie/pkg/mxutil"
5 "fmt"
6 "os"
7 )
8
9 var dotenv map[string]string
10
11 // C holds all relay configuration, loaded from ORLY_* environment variables.
12 // Domain sub-structs are embedded anonymously so existing cfg.Field access
13 // still works, while cfg.AuthCfg, cfg.LimitsCfg, etc. can be passed to
14 // constructors that only need their specific slice.
15 type C struct {
16 // Core identity (not in a sub-struct - every domain needs these)
17 AppName string
18 DataDir string
19 Listen string
20 Port int32
21 HealthPort int32
22 LogLevel string
23 LogToStdout bool
24 LogBufferSize int32
25
26 // Domain config slices - embedded for backward-compatible field promotion.
27 AuthCfg
28 LimitsCfg
29 WorkersCfg
30 StorageCfg
31 BlossomCfg
32 WebCfg
33 IdentityCfg
34
35 // Bunker
36 BunkerEnabled bool
37 BunkerPort int32
38 // Payment
39 NWCUri string
40 SubscriptionEnabled bool
41 MonthlyPriceSats int64
42 // NIP-43
43 NIP43Enabled bool
44 NIP43PublishEvents bool
45 NIP43PublishMembers bool
46 NIP43InviteExpSec int32
47 // Policy
48 PolicyEnabled bool
49 PolicyPath string
50 // Bridge (Marmot)
51 BridgeEnabled bool
52 BridgeDomain string
53 BridgeNSEC string
54 BridgeRelayURL string
55 BridgePublicRelayURL string
56 BridgeSMTPPort int32
57 BridgeSMTPHost string
58 BridgeDataDir string
59 BridgeDKIMKeyPath string
60 BridgeDKIMSelector string
61 BridgeNWCURI string
62 BridgeMonthlyPriceSats int64
63 BridgeComposeURL string
64 BridgeSMTPRelayHost string
65 BridgeSMTPRelayPort int32
66 BridgeSMTPMXPort int32
67 BridgeSMTPRelayUser string
68 BridgeSMTPRelayPass string
69 BridgeAliasPriceSats int64
70 BridgeProfile string
71 // Smesh client
72 SmeshEnabled bool
73 SmeshPort int32
74 Smesh2Enabled bool
75 Smesh2Port int32
76 Smesh3Enabled bool
77 Smesh3Port int32
78 Smesh3Dir string
79 DeployPubkey string
80 // Misc
81 SprocketEnabled bool
82 EnableShutdown bool
83 }
84
85 // Load reads configuration from ORLY_* env vars with optional .env file.
86 // Load reads the environment and the dotenv file once at startup and returns
87 // the config. The stores it makes are package globals, which the rule only
88 // allows from an init-named function, so the body lives in initLoad.
89 func Load() (c *C) {
90 return initLoad()
91 }
92
93 func initLoad() (c *C) {
94 appName := os.Getenv("ORLY_APP_NAME")
95 if appName == "" {
96 appName = "ORLY"
97 }
98 home := os.Getenv("HOME")
99 if home != "" {
100 dotenv = loadEnvFile(home | "/.config/" | appName | "/.env")
101 }
102 c := &C{}
103 c.AppName = estr("ORLY_APP_NAME", "ORLY")
104 c.DataDir = expandHome(estr("ORLY_DATA_DIR", "~/.local/share/ORLY"))
105 c.Listen = estr("ORLY_LISTEN", "0.0.0.0")
106 c.Port = eint("ORLY_PORT", 3334)
107 c.HealthPort = eint("ORLY_HEALTH_PORT", 0)
108 c.LogLevel = estr("ORLY_LOG_LEVEL", "info")
109 c.LogToStdout = ebool("ORLY_LOG_TO_STDOUT", false)
110 c.LogBufferSize = eint("ORLY_LOG_BUFFER_SIZE", 10000)
111 c.RelayURL = estr("ORLY_RELAY_URL", "")
112 c.RelayAddresses = elist("ORLY_RELAY_ADDRESSES")
113 c.RelayPeers = elist("ORLY_RELAY_PEERS")
114 c.SyncPubkey = estr("ORLY_SYNC_PUBKEY", "")
115 c.ClientTag = estr("ORLY_CLIENT_TAG", "git.smesh.lol/morly")
116 c.AuthRequired = ebool("ORLY_AUTH_REQUIRED", false)
117 c.AuthToWrite = ebool("ORLY_AUTH_TO_WRITE", false)
118 c.PrivilegedOpen = ebool("ORLY_PRIVILEGED_OPEN", false)
119 c.NIP70Enforce = ebool("ORLY_NIP70_ENFORCE", true)
120 c.MarmotOpen = ebool("ORLY_MARMOT_OPEN", false)
121 c.NIP46BypassAuth = ebool("ORLY_NIP46_BYPASS_AUTH", false)
122 c.ACLMode = estr("ORLY_ACL_MODE", "none")
123 c.MuteBlacklist = estr("ORLY_MUTE_BLACKLIST", "")
124 c.Admins = elist("ORLY_ADMINS")
125 c.Owners = elist("ORLY_OWNERS")
126 c.FreeWriteLimit = eint("ORLY_FREE_WRITE_LIMIT", 25)
127 c.FreeWriteWindow = eint("ORLY_FREE_WRITE_WINDOW", 300)
128 c.MaxConnPerIP = eint("ORLY_MAX_CONN_PER_IP", 100)
129 c.IngestWorkers = eint("ORLY_INGEST_WORKERS", 0)
130 c.MediaProxyWorkers = eint("ORLY_MEDIA_PROXY_WORKERS", 8)
131 c.BlossomWorkers = eint("ORLY_BLOSSOM_WORKERS", 4)
132 c.MaxGlobalConns = eint("ORLY_MAX_GLOBAL_CONNECTIONS", 500)
133 c.MaxSubscriptions = eint("ORLY_MAX_SUBSCRIPTIONS", 10000)
134 c.ConnDelayMaxMs = eint("ORLY_CONN_DELAY_MAX_MS", 2000)
135 c.IPWhitelist = elist("ORLY_IP_WHITELIST")
136 c.IPBlacklist = elist("ORLY_IP_BLACKLIST")
137 c.HTTPGuardBotBlock = ebool("ORLY_HTTP_GUARD_BOT_BLOCK", true)
138 c.RateLimitEnabled = ebool("ORLY_RATE_LIMIT_ENABLED", true)
139 c.RateLimitTargetMB = eint("ORLY_RATE_LIMIT_TARGET_MB", 0)
140 c.RateLimitWriteKp = efloat("ORLY_RATE_LIMIT_WRITE_KP", 0.5)
141 c.RateLimitWriteKi = efloat("ORLY_RATE_LIMIT_WRITE_KI", 0.1)
142 c.RateLimitWriteKd = efloat("ORLY_RATE_LIMIT_WRITE_KD", 0.05)
143 c.RateLimitReadKp = efloat("ORLY_RATE_LIMIT_READ_KP", 0.3)
144 c.RateLimitReadKi = efloat("ORLY_RATE_LIMIT_READ_KI", 0.05)
145 c.RateLimitReadKd = efloat("ORLY_RATE_LIMIT_READ_KD", 0.02)
146 c.RateLimitMaxWriteMs = eint("ORLY_RATE_LIMIT_MAX_WRITE_MS", 1000)
147 c.RateLimitMaxReadMs = eint("ORLY_RATE_LIMIT_MAX_READ_MS", 500)
148 c.RateLimitWriteTarget = efloat("ORLY_RATE_LIMIT_WRITE_TARGET", 0.85)
149 c.RateLimitReadTarget = efloat("ORLY_RATE_LIMIT_READ_TARGET", 0.90)
150 c.EmergencyThreshold = efloat("ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD", 1.167)
151 c.RecoveryThreshold = efloat("ORLY_RATE_LIMIT_RECOVERY_THRESHOLD", 0.833)
152 c.EmergencyMaxMs = eint("ORLY_RATE_LIMIT_EMERGENCY_MAX_MS", 5000)
153 c.QueryResultLimit = eint("ORLY_QUERY_RESULT_LIMIT", 256)
154 c.FollowListFreqSec = edursec("ORLY_FOLLOW_LIST_FREQUENCY", 3600)
155 c.FollowsThrottle = ebool("ORLY_FOLLOWS_THROTTLE", false)
156 c.FollowsThrottleIncrMs = edurms("ORLY_FOLLOWS_THROTTLE_INCREMENT", 25)
157 c.FollowsThrottleMaxMs = edurms("ORLY_FOLLOWS_THROTTLE_MAX", 60000)
158 c.SocialThrottleD2IncrMs = edurms("ORLY_SOCIAL_THROTTLE_D2_INCREMENT", 50)
159 c.SocialThrottleD2MaxMs = edurms("ORLY_SOCIAL_THROTTLE_D2_MAX", 30000)
160 c.SocialThrottleD3IncrMs = edurms("ORLY_SOCIAL_THROTTLE_D3_INCREMENT", 200)
161 c.SocialThrottleD3MaxMs = edurms("ORLY_SOCIAL_THROTTLE_D3_MAX", 60000)
162 c.SocialThrottleOutsiderIncrMs = edurms("ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT", 500)
163 c.SocialThrottleOutsiderMaxMs = edurms("ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX", 120000)
164 c.SocialWoTMaxDepth = eint("ORLY_SOCIAL_WOT_DEPTH", 3)
165 c.SocialWoTRefreshSec = edursec("ORLY_SOCIAL_WOT_REFRESH", 3600)
166 c.GrapeVineEnabled = ebool("ORLY_GRAPEVINE_ENABLED", false)
167 c.GrapeVineMaxDepth = eint("ORLY_GRAPEVINE_MAX_DEPTH", 6)
168 c.GrapeVineMaxCycles = eint("ORLY_GRAPEVINE_MAX_CYCLES", 20)
169 c.GrapeVineAttenuation = efloat("ORLY_GRAPEVINE_ATTENUATION", 0.8)
170 c.GrapeVineRigor = efloat("ORLY_GRAPEVINE_RIGOR", 0.25)
171 c.GrapeVineFollowConf = efloat("ORLY_GRAPEVINE_FOLLOW_CONFIDENCE", 0.05)
172 c.GrapeVineObservers = elist("ORLY_GRAPEVINE_OBSERVERS")
173 c.GrapeVineRefreshSec = edursec("ORLY_GRAPEVINE_REFRESH", 21600)
174 c.GrapeVineAutoWhitelist = ebool("ORLY_GRAPEVINE_AUTO_WHITELIST", false)
175 c.GrapeVineWhitelistThresh = efloat("ORLY_GRAPEVINE_WHITELIST_THRESHOLD", 0.5)
176 c.GrapeVineWhitelistRefSec = edursec("ORLY_GRAPEVINE_WHITELIST_REFRESH", 21600)
177 c.GraphQueriesEnabled = ebool("ORLY_GRAPH_QUERIES_ENABLED", true)
178 c.GraphMaxDepth = eint("ORLY_GRAPH_MAX_DEPTH", 16)
179 c.GraphMaxResults = eint("ORLY_GRAPH_MAX_RESULTS", 10000)
180 c.GraphRateLimitRPM = eint("ORLY_GRAPH_RATE_LIMIT_RPM", 60)
181 c.ProxyEnabled = ebool("ORLY_PROXY_ENABLED", true)
182 c.ProxyMaxRelays = eint("ORLY_PROXY_MAX_RELAYS", 16)
183 c.ProxyTimeoutSec = eint("ORLY_PROXY_TIMEOUT_SEC", 15)
184 c.ArchiveEnabled = ebool("ORLY_ARCHIVE_ENABLED", false)
185 c.ArchiveRelays = elist("ORLY_ARCHIVE_RELAYS")
186 if len(c.ArchiveRelays) == 0 {
187 c.ArchiveRelays = []string{"wss://archive.orly.dev/"}
188 }
189 c.ArchiveTimeoutSec = eint("ORLY_ARCHIVE_TIMEOUT_SEC", 30)
190 c.ArchiveCacheTTLHrs = eint("ORLY_ARCHIVE_CACHE_TTL_HRS", 24)
191 c.BlossomEnabled = ebool("ORLY_BLOSSOM_ENABLED", true)
192 c.BlossomDir = estr("ORLY_BLOSSOM_DIR", c.DataDir|"/blossom")
193 c.BlossomUpstream = estr("ORLY_BLOSSOM_UPSTREAM", "")
194 c.BlossomServiceLevels = estr("ORLY_BLOSSOM_SERVICE_LEVELS", "")
195 c.BlossomRateLimit = ebool("ORLY_BLOSSOM_RATE_LIMIT", false)
196 c.BlossomDailyLimitMB = eint64("ORLY_BLOSSOM_DAILY_LIMIT_MB", 10)
197 c.BlossomBurstLimitMB = eint64("ORLY_BLOSSOM_BURST_LIMIT_MB", 50)
198 c.BlossomDeleteRequireServerTag = ebool("ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG", false)
199 c.BootstrapRelays = elist("ORLY_BOOTSTRAP_RELAYS")
200 c.SpiderMode = estr("ORLY_SPIDER_MODE", "none")
201 c.DirectorySpider = ebool("ORLY_DIRECTORY_SPIDER", false)
202 c.DirectorySpiderIntSec = edursec("ORLY_DIRECTORY_SPIDER_INTERVAL", 86400)
203 c.DirectorySpiderMaxHops = eint("ORLY_DIRECTORY_SPIDER_HOPS", 3)
204 c.CrawlerEnabled = ebool("ORLY_CRAWLER_ENABLED", false)
205 c.CrawlerDiscoveryIntSec = edursec("ORLY_CRAWLER_DISCOVERY_INTERVAL", 14400)
206 c.CrawlerSyncIntSec = edursec("ORLY_CRAWLER_SYNC_INTERVAL", 1800)
207 c.CrawlerMaxHops = eint("ORLY_CRAWLER_MAX_HOPS", 5)
208 c.CrawlerConcurrency = eint("ORLY_CRAWLER_CONCURRENCY", 3)
209 c.NegentropyEnabled = ebool("ORLY_NEGENTROPY_ENABLED", false)
210 c.NegentropyFullSyncPubs = estr("ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS", "")
211 c.TLSDomains = elist("ORLY_TLS_DOMAINS")
212 c.Certs = elist("ORLY_CERTS")
213 c.TorEnabled = ebool("ORLY_TOR_ENABLED", true)
214 c.TorPort = eint("ORLY_TOR_PORT", 3336)
215 c.TorDataDir = estr("ORLY_TOR_DATA_DIR", "")
216 c.TorBinary = estr("ORLY_TOR_BINARY", "tor")
217 c.TorSOCKS = eint("ORLY_TOR_SOCKS", 0)
218 c.NRCEnabled = ebool("ORLY_NRC_ENABLED", true)
219 c.NRCRendezvousURL = estr("ORLY_NRC_RENDEZVOUS_URL", "")
220 c.NRCAuthorizedKeys = estr("ORLY_NRC_AUTHORIZED_KEYS", "")
221 c.NRCSessionTimeSec = edursec("ORLY_NRC_SESSION_TIMEOUT", 1800)
222 c.WGEnabled = ebool("ORLY_WG_ENABLED", false)
223 c.WGPort = eint("ORLY_WG_PORT", 51820)
224 c.WGEndpoint = estr("ORLY_WG_ENDPOINT", "")
225 c.WGNetwork = estr("ORLY_WG_NETWORK", "10.73.0.0/16")
226 c.BunkerEnabled = ebool("ORLY_BUNKER_ENABLED", false)
227 c.BunkerPort = eint("ORLY_BUNKER_PORT", 3335)
228 c.NWCUri = estr("ORLY_NWC_URI", "")
229 c.SubscriptionEnabled = ebool("ORLY_SUBSCRIPTION_ENABLED", false)
230 c.MonthlyPriceSats = eint64("ORLY_MONTHLY_PRICE_SATS", 6000)
231 c.NIP43Enabled = ebool("ORLY_NIP43_ENABLED", false)
232 c.NIP43PublishEvents = ebool("ORLY_NIP43_PUBLISH_EVENTS", true)
233 c.NIP43PublishMembers = ebool("ORLY_NIP43_PUBLISH_MEMBER_LIST", true)
234 c.NIP43InviteExpSec = edursec("ORLY_NIP43_INVITE_EXPIRY", 86400)
235 c.PolicyEnabled = ebool("ORLY_POLICY_ENABLED", false)
236 c.PolicyPath = estr("ORLY_POLICY_PATH", "")
237 c.MaxStorageBytes = eint64("ORLY_MAX_STORAGE_BYTES", 0)
238 c.GCEnabled = ebool("ORLY_GC_ENABLED", false)
239 c.GCIntervalSec = eint("ORLY_GC_INTERVAL_SEC", 60)
240 c.GCBatchSize = eint("ORLY_GC_BATCH_SIZE", 1000)
241 c.BridgeEnabled = ebool("ORLY_BRIDGE_ENABLED", false)
242 c.BridgeDomain = estr("ORLY_BRIDGE_DOMAIN", "")
243 c.BridgeNSEC = estr("ORLY_BRIDGE_NSEC", "")
244 c.BridgeRelayURL = estr("ORLY_BRIDGE_RELAY_URL", "")
245 c.BridgePublicRelayURL = estr("ORLY_BRIDGE_PUBLIC_RELAY_URL", "")
246 c.BridgeSMTPPort = eint("ORLY_BRIDGE_SMTP_PORT", 2525)
247 c.BridgeSMTPHost = estr("ORLY_BRIDGE_SMTP_HOST", "0.0.0.0")
248 c.BridgeDataDir = estr("ORLY_BRIDGE_DATA_DIR", "")
249 c.BridgeDKIMKeyPath = estr("ORLY_BRIDGE_DKIM_KEY", "")
250 c.BridgeDKIMSelector = estr("ORLY_BRIDGE_DKIM_SELECTOR", "marmot")
251 c.BridgeNWCURI = estr("ORLY_BRIDGE_NWC_URI", "")
252 c.BridgeMonthlyPriceSats = eint64("ORLY_BRIDGE_MONTHLY_PRICE_SATS", 2100)
253 c.BridgeComposeURL = estr("ORLY_BRIDGE_COMPOSE_URL", "")
254 c.BridgeSMTPRelayHost = estr("ORLY_BRIDGE_SMTP_RELAY_HOST", "")
255 c.BridgeSMTPRelayPort = eint("ORLY_BRIDGE_SMTP_RELAY_PORT", 587)
256 c.BridgeSMTPMXPort = eint("ORLY_BRIDGE_SMTP_MX_PORT", 0)
257 c.BridgeSMTPRelayUser = estr("ORLY_BRIDGE_SMTP_RELAY_USERNAME", "")
258 c.BridgeSMTPRelayPass = estr("ORLY_BRIDGE_SMTP_RELAY_PASSWORD", "")
259 c.BridgeAliasPriceSats = eint64("ORLY_BRIDGE_ALIAS_PRICE_SATS", 4200)
260 c.BridgeProfile = estr("ORLY_BRIDGE_PROFILE", "")
261 c.ClusterAdmins = elist("ORLY_CLUSTER_ADMINS")
262 c.RelayGroupAdmins = elist("ORLY_RELAY_GROUP_ADMINS")
263 c.ClusterPropPrivileged = ebool("ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS", true)
264 c.SmeshEnabled = ebool("ORLY_SMESH_ENABLED", true)
265 c.SmeshPort = eint("ORLY_SMESH_PORT", 8088)
266 c.Smesh2Enabled = ebool("ORLY_SMESH2_ENABLED", true)
267 c.Smesh2Port = eint("ORLY_SMESH2_PORT", 8089)
268 c.Smesh3Enabled = ebool("ORLY_SMESH3_ENABLED", true)
269 c.Smesh3Port = eint("ORLY_SMESH3_PORT", 8090)
270 c.Smesh3Dir = estr("ORLY_SMESH3_DIR", "")
271 c.DeployPubkey = estr("ORLY_DEPLOY_PUBKEY", "")
272 c.StaticDir = estr("ORLY_STATIC_DIR", "web/static")
273 c.WebDisable = ebool("ORLY_WEB_DISABLE", false)
274 c.WebDevProxyURL = estr("ORLY_WEB_DEV_PROXY_URL", "")
275 c.BrandingDir = estr("ORLY_BRANDING_DIR", "")
276 c.BrandingEnabled = ebool("ORLY_BRANDING_ENABLED", true)
277 c.Theme = estr("ORLY_THEME", "auto")
278 c.CORSEnabled = ebool("ORLY_CORS_ENABLED", false)
279 c.CORSOrigins = elist("ORLY_CORS_ORIGINS")
280 c.SprocketEnabled = ebool("ORLY_SPROCKET_ENABLED", false)
281 c.EnableShutdown = ebool("ORLY_ENABLE_SHUTDOWN", false)
282 return c
283 }
284
285 // Addr returns "listen:port".
286 func (c *C) Addr() (s string) {
287 for i := 0; i < len(c.Listen); i++ {
288 if c.Listen[i] == ':' {
289 return c.Listen
290 }
291 }
292 return c.Listen | ":" | itoa(c.Port)
293 }
294
295 // PrintHelp writes all env var documentation.
296 func PrintHelp() {
297 w := os.Stderr
298 fmt.Fprintln(w, "ORLY - Nostr relay")
299 fmt.Fprintln(w, "")
300 fmt.Fprintln(w, "Usage: musiquay [relay|sync|crawl|env|help|version]")
301 fmt.Fprintln(w, "")
302 fmt.Fprintln(w, "Environment variables:")
303 s := func(title string) { fmt.Fprintf(w, "\n %s\n", title) }
304 p := func(name, def, desc string) { fmt.Fprintf(w, " %-48s %s (default: %s)\n", name, desc, def) }
305 s("Core")
306 p("ORLY_APP_NAME", "ORLY", "relay display name")
307 p("ORLY_DATA_DIR", "~/.local/share/ORLY", "event store location")
308 p("ORLY_LISTEN", "0.0.0.0", "listen address")
309 p("ORLY_PORT", "3334", "listen port")
310 p("ORLY_HEALTH_PORT", "0", "health check port (0=disabled)")
311 p("ORLY_LOG_LEVEL", "info", "log level: fatal error warn info debug trace")
312 p("ORLY_LOG_TO_STDOUT", "false", "log to stdout instead of stderr")
313 p("ORLY_LOG_BUFFER_SIZE", "10000", "log entries kept for web UI")
314 s("Relay Identity")
315 p("ORLY_RELAY_URL", "", "base URL (e.g. https://relay.example.com)")
316 p("ORLY_RELAY_ADDRESSES", "", "websocket addresses (comma-separated)")
317 p("ORLY_RELAY_PEERS", "", "peer relay URLs (comma-separated)")
318 p("ORLY_CLIENT_TAG", "git.smesh.lol/morly", "client tag for published events")
319 s("Auth & Access")
320 p("ORLY_AUTH_REQUIRED", "false", "require auth for all requests")
321 p("ORLY_AUTH_TO_WRITE", "false", "require auth for writes only")
322 p("ORLY_PRIVILEGED_OPEN", "false", "disable privileged-kind auth checks")
323 p("ORLY_NIP70_ENFORCE", "true", "enforce NIP-70 protected tag broadcast filter (false = relay all)")
324 p("ORLY_MARMOT_OPEN", "false", "exempt MLS kinds from auth (443,444,445)")
325 p("ORLY_NIP46_BYPASS_AUTH", "false", "allow NIP-46 (kind 24133) without auth")
326 p("ORLY_ACL_MODE", "none", "ACL mode: follows, managed, curating, none")
327 p("ORLY_MUTE_BLACKLIST", "", "hex pubkey whose mute list (kind 10000) bans authors")
328 p("ORLY_ADMINS", "", "admin npubs (comma-separated)")
329 p("ORLY_OWNERS", "", "owner npubs (comma-separated)")
330 p("ORLY_FREE_WRITE_LIMIT", "25", "max unauthenticated writes per IP per window (0=disable)")
331 p("ORLY_FREE_WRITE_WINDOW", "300", "free write window in seconds")
332 s("Connection Limits")
333 p("ORLY_MAX_CONN_PER_IP", "10", "max WebSocket connections per IP")
334 p("ORLY_MAX_GLOBAL_CONNECTIONS", "500", "max total WebSocket connections")
335 p("ORLY_MAX_SUBSCRIPTIONS", "10000", "max total active subscriptions")
336 p("ORLY_INGEST_WORKERS", "0", "Stage-A sig-verify worker count (0=sync fallback)")
337 p("ORLY_MEDIA_PROXY_WORKERS", "4", "media proxy worker count (503 when all busy)")
338 p("ORLY_BLOSSOM_WORKERS", "4", "blossom file I/O worker count")
339 p("ORLY_STATIC_WORKERS", "1", "static file worker count")
340 p("ORLY_CONN_DELAY_MAX_MS", "2000", "max delay for new connections under load")
341 s("IP Control")
342 p("ORLY_IP_WHITELIST", "", "allowed IPs (comma-separated, prefix match)")
343 p("ORLY_IP_BLACKLIST", "", "blocked IPs (comma-separated, prefix match)")
344 s("HTTP Guard")
345 p("ORLY_HTTP_GUARD_BOT_BLOCK", "true", "block known bot User-Agents")
346 s("Rate Limiting (PID)")
347 p("ORLY_RATE_LIMIT_ENABLED", "true", "enable adaptive PID rate limiting")
348 p("ORLY_RATE_LIMIT_TARGET_MB", "0", "target memory limit (0=auto)")
349 p("ORLY_RATE_LIMIT_WRITE_KP", "0.5", "PID proportional gain for writes")
350 p("ORLY_RATE_LIMIT_WRITE_KI", "0.1", "PID integral gain for writes")
351 p("ORLY_RATE_LIMIT_WRITE_KD", "0.05", "PID derivative gain for writes")
352 p("ORLY_RATE_LIMIT_READ_KP", "0.3", "PID proportional gain for reads")
353 p("ORLY_RATE_LIMIT_READ_KI", "0.05", "PID integral gain for reads")
354 p("ORLY_RATE_LIMIT_READ_KD", "0.02", "PID derivative gain for reads")
355 p("ORLY_RATE_LIMIT_MAX_WRITE_MS", "1000", "max write delay (ms)")
356 p("ORLY_RATE_LIMIT_MAX_READ_MS", "500", "max read delay (ms)")
357 p("ORLY_RATE_LIMIT_WRITE_TARGET", "0.85", "write throttle setpoint")
358 p("ORLY_RATE_LIMIT_READ_TARGET", "0.90", "read throttle setpoint")
359 p("ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD", "1.167", "emergency mode trigger ratio")
360 p("ORLY_RATE_LIMIT_RECOVERY_THRESHOLD", "0.833", "emergency mode exit ratio")
361 p("ORLY_RATE_LIMIT_EMERGENCY_MAX_MS", "5000", "max delay in emergency mode")
362 s("Query")
363 p("ORLY_QUERY_RESULT_LIMIT", "256", "max events per REQ filter")
364 s("Follows ACL")
365 p("ORLY_FOLLOW_LIST_FREQUENCY", "1h", "admin follow list refresh interval")
366 p("ORLY_FOLLOWS_THROTTLE", "false", "enable progressive delay for non-followed")
367 p("ORLY_FOLLOWS_THROTTLE_INCREMENT", "25ms", "delay per event for non-followed")
368 p("ORLY_FOLLOWS_THROTTLE_MAX", "60s", "max throttle delay")
369 s("Social WoT Throttle")
370 p("ORLY_SOCIAL_THROTTLE_D2_INCREMENT", "50ms", "delay per event for WoT depth-2")
371 p("ORLY_SOCIAL_THROTTLE_D2_MAX", "30s", "max delay for WoT depth-2")
372 p("ORLY_SOCIAL_THROTTLE_D3_INCREMENT", "200ms", "delay per event for WoT depth-3")
373 p("ORLY_SOCIAL_THROTTLE_D3_MAX", "60s", "max delay for WoT depth-3")
374 p("ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT", "500ms", "delay per event for outsiders")
375 p("ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX", "120s", "max delay for outsiders")
376 p("ORLY_SOCIAL_WOT_DEPTH", "3", "max WoT traversal depth")
377 p("ORLY_SOCIAL_WOT_REFRESH", "1h", "WoT depth map recompute interval")
378 s("GrapeVine")
379 p("ORLY_GRAPEVINE_ENABLED", "false", "enable WoT influence scoring")
380 p("ORLY_GRAPEVINE_MAX_DEPTH", "6", "max BFS depth for follow graph")
381 p("ORLY_GRAPEVINE_MAX_CYCLES", "20", "max convergence iterations")
382 p("ORLY_GRAPEVINE_ATTENUATION", "0.8", "weight decay per hop")
383 p("ORLY_GRAPEVINE_RIGOR", "0.25", "certainty curve steepness")
384 p("ORLY_GRAPEVINE_FOLLOW_CONFIDENCE", "0.05", "base confidence per follow edge")
385 p("ORLY_GRAPEVINE_OBSERVERS", "", "hex pubkeys for auto-scoring")
386 p("ORLY_GRAPEVINE_REFRESH", "6h", "recalculation interval")
387 p("ORLY_GRAPEVINE_AUTO_WHITELIST", "false", "auto-update ACL from scores")
388 p("ORLY_GRAPEVINE_WHITELIST_THRESHOLD", "0.5", "min score for whitelist")
389 p("ORLY_GRAPEVINE_WHITELIST_REFRESH", "6h", "whitelist refresh interval")
390 s("Graph Queries")
391 p("ORLY_GRAPH_QUERIES_ENABLED", "true", "enable _graph filter extension")
392 p("ORLY_GRAPH_MAX_DEPTH", "16", "max graph traversal depth")
393 p("ORLY_GRAPH_MAX_RESULTS", "10000", "max results per graph query")
394 p("ORLY_GRAPH_RATE_LIMIT_RPM", "60", "graph queries per minute per conn")
395 s("Proxy")
396 p("ORLY_PROXY_ENABLED", "true", "enable _proxy filter extension")
397 p("ORLY_PROXY_MAX_RELAYS", "16", "max relay URLs per proxy query")
398 p("ORLY_PROXY_TIMEOUT_SEC", "15", "proxy relay query timeout")
399 s("Archive")
400 p("ORLY_ARCHIVE_ENABLED", "false", "enable archive relay augmentation")
401 p("ORLY_ARCHIVE_RELAYS", "wss://archive.orly.dev/", "archive relay URLs")
402 p("ORLY_ARCHIVE_TIMEOUT_SEC", "30", "archive relay query timeout")
403 p("ORLY_ARCHIVE_CACHE_TTL_HRS", "24", "hours to cache query fingerprints")
404 s("Blossom")
405 p("ORLY_BLOSSOM_ENABLED", "true", "enable blob storage server")
406 p("ORLY_BLOSSOM_DIR", "$DATA_DIR/blossom", "blob storage directory")
407 p("ORLY_BLOSSOM_UPSTREAM", "https://smesh.lol", "CORS-proxy fallback origin for missing blobs (empty disables)")
408 p("ORLY_BLOSSOM_SERVICE_LEVELS", "", "service levels (name:mb_per_sat)")
409 p("ORLY_BLOSSOM_RATE_LIMIT", "false", "rate-limit non-followed uploads")
410 p("ORLY_BLOSSOM_DAILY_LIMIT_MB", "10", "daily upload limit for non-followed")
411 p("ORLY_BLOSSOM_BURST_LIMIT_MB", "50", "burst upload limit")
412 p("ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG", "false", "require server tag in delete auth")
413 s("Sync & Discovery")
414 p("ORLY_BOOTSTRAP_RELAYS", "", "bootstrap relay URLs (comma-separated)")
415 p("ORLY_SPIDER_MODE", "none", "spider mode: none, follows")
416 p("ORLY_DIRECTORY_SPIDER", "false", "enable directory metadata sync")
417 p("ORLY_DIRECTORY_SPIDER_INTERVAL", "24h", "directory spider interval")
418 p("ORLY_DIRECTORY_SPIDER_HOPS", "3", "max relay discovery hops")
419 p("ORLY_CRAWLER_ENABLED", "false", "enable corpus crawler")
420 p("ORLY_CRAWLER_DISCOVERY_INTERVAL", "4h", "relay discovery interval")
421 p("ORLY_CRAWLER_SYNC_INTERVAL", "30m", "relay sync interval")
422 p("ORLY_CRAWLER_MAX_HOPS", "5", "max hops for relay discovery")
423 p("ORLY_CRAWLER_CONCURRENCY", "3", "concurrent relay syncs")
424 p("ORLY_NEGENTROPY_ENABLED", "false", "enable NIP-77 set reconciliation")
425 p("ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS", "", "pubkeys allowed full sync")
426 s("TLS")
427 p("ORLY_TLS_DOMAINS", "", "TLS domain names (comma-separated)")
428 p("ORLY_CERTS", "", "cert root paths (comma-separated)")
429 s("Tor")
430 p("ORLY_TOR_ENABLED", "true", "enable Tor hidden service")
431 p("ORLY_TOR_PORT", "3336", "Tor internal port")
432 p("ORLY_TOR_DATA_DIR", "", "Tor data directory")
433 p("ORLY_TOR_BINARY", "tor", "path to tor binary")
434 p("ORLY_TOR_SOCKS", "0", "SOCKS port for outbound Tor")
435 s("NRC")
436 p("ORLY_NRC_ENABLED", "true", "enable NRC rendezvous bridge")
437 p("ORLY_NRC_RENDEZVOUS_URL", "", "rendezvous relay URL")
438 p("ORLY_NRC_AUTHORIZED_KEYS", "", "authorized client pubkeys")
439 p("ORLY_NRC_SESSION_TIMEOUT", "30m", "NRC session inactivity timeout")
440 s("WireGuard")
441 p("ORLY_WG_ENABLED", "false", "enable embedded WireGuard VPN")
442 p("ORLY_WG_PORT", "51820", "WireGuard UDP port")
443 p("ORLY_WG_ENDPOINT", "", "WireGuard public endpoint")
444 p("ORLY_WG_NETWORK", "10.73.0.0/16", "WireGuard internal network")
445 s("Bunker")
446 p("ORLY_BUNKER_ENABLED", "false", "enable NIP-46 bunker service")
447 p("ORLY_BUNKER_PORT", "3335", "bunker WebSocket port")
448 s("Payment")
449 p("ORLY_NWC_URI", "", "NWC connection string")
450 p("ORLY_SUBSCRIPTION_ENABLED", "false", "enable subscription access")
451 p("ORLY_MONTHLY_PRICE_SATS", "6000", "monthly subscription price")
452 s("NIP-43")
453 p("ORLY_NIP43_ENABLED", "false", "enable relay access metadata")
454 p("ORLY_NIP43_PUBLISH_EVENTS", "true", "publish member add/remove events")
455 p("ORLY_NIP43_PUBLISH_MEMBER_LIST", "true", "publish membership list events")
456 p("ORLY_NIP43_INVITE_EXPIRY", "24h", "invite code validity period")
457 s("Policy")
458 p("ORLY_POLICY_ENABLED", "false", "enable policy-based event processing")
459 p("ORLY_POLICY_PATH", "", "absolute path to policy JSON file")
460 s("Storage & GC")
461 p("ORLY_MAX_STORAGE_BYTES", "0", "max storage bytes (0=auto 80%%)")
462 p("ORLY_GC_ENABLED", "false", "enable garbage collection")
463 p("ORLY_GC_INTERVAL_SEC", "60", "GC run interval")
464 p("ORLY_GC_BATCH_SIZE", "1000", "events per GC run")
465 s("Bridge (Marmot)")
466 p("ORLY_BRIDGE_ENABLED", "false", "enable Nostr-Email bridge")
467 p("ORLY_BRIDGE_DOMAIN", "", "email domain for bridge")
468 p("ORLY_BRIDGE_NSEC", "", "bridge identity nsec")
469 p("ORLY_BRIDGE_RELAY_URL", "", "relay URL for standalone mode")
470 p("ORLY_BRIDGE_PUBLIC_RELAY_URL", "", "public relay URL for events")
471 p("ORLY_BRIDGE_SMTP_PORT", "2525", "SMTP listen port")
472 p("ORLY_BRIDGE_SMTP_HOST", "0.0.0.0", "SMTP listen address")
473 p("ORLY_BRIDGE_DATA_DIR", "", "bridge data directory")
474 p("ORLY_BRIDGE_DKIM_KEY", "", "DKIM private key path")
475 p("ORLY_BRIDGE_DKIM_SELECTOR", "marmot", "DKIM selector")
476 p("ORLY_BRIDGE_NWC_URI", "", "NWC URI for bridge payments")
477 p("ORLY_BRIDGE_MONTHLY_PRICE_SATS", "2100", "bridge subscription price")
478 p("ORLY_BRIDGE_COMPOSE_URL", "", "compose form URL")
479 p("ORLY_BRIDGE_SMTP_RELAY_HOST", "", "SMTP smarthost")
480 p("ORLY_BRIDGE_SMTP_RELAY_PORT", "587", "SMTP smarthost port")
481 p("ORLY_BRIDGE_SMTP_MX_PORT", "0", "direct MX port (0=auto)")
482 p("ORLY_BRIDGE_SMTP_RELAY_USERNAME", "", "SMTP smarthost username")
483 p("ORLY_BRIDGE_SMTP_RELAY_PASSWORD", "", "SMTP smarthost password")
484 p("ORLY_BRIDGE_ALIAS_PRICE_SATS", "4200", "alias email monthly price")
485 p("ORLY_BRIDGE_PROFILE", "", "bridge profile template path")
486 s("Cluster")
487 p("ORLY_CLUSTER_ADMINS", "", "cluster admin npubs")
488 p("ORLY_RELAY_GROUP_ADMINS", "", "relay group admin npubs")
489 p("ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS", "true", "replicate privileged events")
490 s("Smesh Client")
491 p("ORLY_SMESH_ENABLED", "true", "enable musiquay web client")
492 p("ORLY_SMESH_PORT", "8088", "musiquay client port")
493 p("ORLY_SMESH2_ENABLED", "true", "enable musiquay2 client")
494 p("ORLY_SMESH2_PORT", "8089", "musiquay2 client port")
495 p("ORLY_SMESH3_ENABLED", "true", "enable musiquay3 client")
496 p("ORLY_SMESH3_PORT", "8090", "musiquay3 client port")
497 p("ORLY_SMESH3_DIR", "", "musiquay3 disk directory (hot-reload)")
498 p("ORLY_DEPLOY_PUBKEY", "", "deploy asset bundle pubkey")
499 s("Web UI")
500 p("ORLY_STATIC_DIR", "web/static", "static file directory")
501 p("ORLY_WEB_DISABLE", "false", "disable embedded web UI")
502 p("ORLY_WEB_DEV_PROXY_URL", "", "dev proxy URL when UI disabled")
503 p("ORLY_BRANDING_DIR", "", "branding assets directory")
504 p("ORLY_BRANDING_ENABLED", "true", "enable custom branding")
505 p("ORLY_THEME", "auto", "UI theme: auto, light, dark")
506 p("ORLY_CORS_ENABLED", "false", "enable CORS headers")
507 p("ORLY_CORS_ORIGINS", "", "allowed CORS origins")
508 s("Misc")
509 p("ORLY_SPROCKET_ENABLED", "false", "enable sprocket plugin system")
510 p("ORLY_ENABLE_SHUTDOWN", "false", "expose /shutdown on health port")
511 fmt.Fprintln(w, "")
512 }
513
514 // --- env helpers ---
515
516 func estr(key, fb string) (s string) {
517 if v := os.Getenv(key); v != "" {
518 return v
519 }
520 if dotenv != nil {
521 if v, ok := dotenv[key]; ok && v != "" {
522 return v
523 }
524 }
525 return fb
526 }
527
528 func eint(key string, fb int32) (n int32) {
529 v := estr(key, "")
530 if v == "" {
531 return fb
532 }
533 n, ok := parseInt(v)
534 if !ok {
535 return fb
536 }
537 return n
538 }
539
540 func eint64(key string, fb int64) (n int64) {
541 v := estr(key, "")
542 if v == "" {
543 return fb
544 }
545 n, ok := parseInt64(v)
546 if !ok {
547 return fb
548 }
549 return n
550 }
551
552 func ebool(key string, fb bool) (ok bool) {
553 v := estr(key, "")
554 if v == "" {
555 return fb
556 }
557 return v == "true" || v == "True" || v == "TRUE" ||
558 v == "1" || v == "yes" || v == "Yes" || v == "YES"
559 }
560
561 func efloat(key string, fb float64) (f float64) {
562 v := estr(key, "")
563 if v == "" {
564 return fb
565 }
566 f, ok := parseFloat(v)
567 if !ok {
568 return fb
569 }
570 return f
571 }
572
573 func elist(key string) (ss []string) {
574 v := estr(key, "")
575 if v == "" {
576 return nil
577 }
578 return splitComma(v)
579 }
580
581 func edursec(key string, fb int32) (n int32) {
582 v := estr(key, "")
583 if v == "" {
584 return fb
585 }
586 ms := parseDuration(v)
587 if ms <= 0 {
588 return fb
589 }
590 return ms / 1000
591 }
592
593 func edurms(key string, fb int32) (n int32) {
594 v := estr(key, "")
595 if v == "" {
596 return fb
597 }
598 ms := parseDuration(v)
599 if ms <= 0 {
600 return fb
601 }
602 return ms
603 }
604
605 // --- parsing ---
606
607 func parseInt(s string) (n int32, ok bool) {
608 if len(s) == 0 {
609 return 0, false
610 }
611 neg := false
612 i := 0
613 if s[0] == '-' {
614 neg = true
615 i = 1
616 }
617 n := 0
618 for ; i < len(s); i++ {
619 if s[i] < '0' || s[i] > '9' {
620 return 0, false
621 }
622 n = n*10 + int32(s[i]-'0')
623 }
624 if neg {
625 return -n, true
626 }
627 return n, true
628 }
629
630 func parseInt64(s string) (n int64, ok bool) {
631 if len(s) == 0 {
632 return 0, false
633 }
634 neg := false
635 i := 0
636 if s[0] == '-' {
637 neg = true
638 i = 1
639 }
640 var n int64
641 for ; i < len(s); i++ {
642 if s[i] < '0' || s[i] > '9' {
643 return 0, false
644 }
645 n = n*10 + int64(s[i]-'0')
646 }
647 if neg {
648 return -n, true
649 }
650 return n, true
651 }
652
653 func parseFloat(s string) (f float64, ok bool) {
654 if len(s) == 0 {
655 return 0, false
656 }
657 neg := false
658 i := 0
659 if s[0] == '-' {
660 neg = true
661 i = 1
662 } else if s[0] == '+' {
663 i = 1
664 }
665 var integer float64
666 for ; i < len(s) && s[i] != '.'; i++ {
667 if s[i] < '0' || s[i] > '9' {
668 return 0, false
669 }
670 integer = integer*10 + float64(s[i]-'0')
671 }
672 var frac float64
673 if i < len(s) && s[i] == '.' {
674 i++
675 mul := 0.1
676 for ; i < len(s); i++ {
677 if s[i] < '0' || s[i] > '9' {
678 return 0, false
679 }
680 frac += float64(s[i]-'0') * mul
681 mul *= 0.1
682 }
683 }
684 result := integer + frac
685 if neg {
686 result = -result
687 }
688 return result, true
689 }
690
691 // parseDuration handles "1h", "30m", "60s", "25ms" and returns milliseconds.
692 func parseDuration(s string) (n int32) {
693 if len(s) == 0 {
694 return 0
695 }
696 i := 0
697 for i < len(s) && s[i] >= '0' && s[i] <= '9' {
698 i++
699 }
700 if i == 0 {
701 return 0
702 }
703 n, ok := parseInt(s[:i])
704 if !ok {
705 return 0
706 }
707 unit := s[i:]
708 switch unit {
709 case "h":
710 return n * 3600000
711 case "m":
712 return n * 60000
713 case "s", "":
714 return n * 1000
715 case "ms":
716 return n
717 }
718 return n * 1000
719 }
720
721 // --- .env file ---
722
723 func loadEnvFile(path string) (m map[string]string) {
724 data, err := os.ReadFile(path)
725 if err != nil {
726 return nil
727 }
728 m := map[string]string{}
729 for len(data) > 0 {
730 nl := -1
731 for i := 0; i < len(data); i++ {
732 if data[i] == '\n' {
733 nl = i
734 break
735 }
736 }
737 var line []byte
738 if nl >= 0 {
739 line = data[:nl]
740 data = data[nl+1:]
741 } else {
742 line = data
743 data = nil
744 }
745 if len(line) > 0 && line[len(line)-1] == '\r' {
746 line = line[:len(line)-1]
747 }
748 line = trim(line)
749 if len(line) == 0 || line[0] == '#' {
750 continue
751 }
752 if len(line) > 7 && string(line[:7]) == "export " {
753 line = trim(line[7:])
754 }
755 eq := -1
756 for i := 0; i < len(line); i++ {
757 if line[i] == '=' {
758 eq = i
759 break
760 }
761 }
762 if eq < 0 {
763 continue
764 }
765 key := copyb(trim(line[:eq]))
766 val := copyb(trim(line[eq+1:]))
767 if len(val) >= 2 {
768 if (val[0] == '"' && val[len(val)-1] == '"') ||
769 (val[0] == '\'' && val[len(val)-1] == '\'') {
770 val = copyb(val[1 : len(val)-1])
771 }
772 }
773 m[string(key)] = string(val)
774 }
775 return m
776 }
777
778 // --- utility ---
779
780 func expandHome(path string) (s string) {
781 if len(path) >= 2 && path[0] == '~' && path[1] == '/' {
782 home := os.Getenv("HOME")
783 if home != "" {
784 return home | path[1:]
785 }
786 }
787 return path
788 }
789
790 func splitComma(s string) (ss []string) {
791 var result []string
792 start := 0
793 for i := 0; i <= len(s); i++ {
794 if i == len(s) || s[i] == ',' {
795 part := trim([]byte(s[start:i]))
796 if len(part) > 0 {
797 result = mxutil.Ensure(result, 1)
798 result = push(result, string(copyb(part)))
799 }
800 start = i + 1
801 }
802 }
803 return result
804 }
805
806 func trim(b []byte) (buf []byte) {
807 for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
808 b = b[1:]
809 }
810 for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t') {
811 b = b[:len(b)-1]
812 }
813 return b
814 }
815
816 func copyb(b []byte) (buf []byte) {
817 c := []byte{:len(b)}
818 copy(c, b)
819 return c
820 }
821
822 func itoa(n int32) (s string) {
823 if n == 0 {
824 return "0"
825 }
826 neg := false
827 if n < 0 {
828 neg = true
829 n = -n
830 }
831 var buf [20]byte
832 i := 19
833 for n > 0 {
834 buf[i] = byte('0' + n%10)
835 i--
836 n /= 10
837 }
838 if neg {
839 buf[i] = '-'
840 i--
841 }
842 return string(buf[i+1:])
843 }
844