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