package config import ( "fmt" "os" ) var dotenv map[string]string // C holds all relay configuration, loaded from ORLY_* environment variables. // Domain sub-structs are embedded anonymously so existing cfg.Field access // still works, while cfg.AuthCfg, cfg.LimitsCfg, etc. can be passed to // constructors that only need their specific slice. type C struct { // Core identity (not in a sub-struct — every domain needs these) AppName string DataDir string Listen string Port int HealthPort int LogLevel string LogToStdout bool LogBufferSize int // Domain config slices — embedded for backward-compatible field promotion. AuthCfg LimitsCfg WorkersCfg StorageCfg BlossomCfg WebCfg IdentityCfg // Bunker BunkerEnabled bool BunkerPort int // Payment NWCUri string SubscriptionEnabled bool MonthlyPriceSats int64 // NIP-43 NIP43Enabled bool NIP43PublishEvents bool NIP43PublishMembers bool NIP43InviteExpSec int // Policy PolicyEnabled bool PolicyPath string // Bridge (Marmot) BridgeEnabled bool BridgeDomain string BridgeNSEC string BridgeRelayURL string BridgePublicRelayURL string BridgeSMTPPort int BridgeSMTPHost string BridgeDataDir string BridgeDKIMKeyPath string BridgeDKIMSelector string BridgeNWCURI string BridgeMonthlyPriceSats int64 BridgeComposeURL string BridgeSMTPRelayHost string BridgeSMTPRelayPort int BridgeSMTPMXPort int BridgeSMTPRelayUser string BridgeSMTPRelayPass string BridgeAliasPriceSats int64 BridgeProfile string // Smesh client SmeshEnabled bool SmeshPort int Smesh2Enabled bool Smesh2Port int Smesh3Enabled bool Smesh3Port int Smesh3Dir string DeployPubkey string // Misc SprocketEnabled bool EnableShutdown bool } // Load reads configuration from ORLY_* env vars with optional .env file. func Load() *C { appName := os.Getenv("ORLY_APP_NAME") if appName == "" { appName = "ORLY" } home := os.Getenv("HOME") if home != "" { dotenv = loadEnvFile(home | "/.config/" | appName | "/.env") } c := &C{} c.AppName = estr("ORLY_APP_NAME", "ORLY") c.DataDir = expandHome(estr("ORLY_DATA_DIR", "~/.local/share/ORLY")) c.Listen = estr("ORLY_LISTEN", "0.0.0.0") c.Port = eint("ORLY_PORT", 3334) c.HealthPort = eint("ORLY_HEALTH_PORT", 0) c.LogLevel = estr("ORLY_LOG_LEVEL", "info") c.LogToStdout = ebool("ORLY_LOG_TO_STDOUT", false) c.LogBufferSize = eint("ORLY_LOG_BUFFER_SIZE", 10000) c.RelayURL = estr("ORLY_RELAY_URL", "") c.RelayAddresses = elist("ORLY_RELAY_ADDRESSES") c.RelayPeers = elist("ORLY_RELAY_PEERS") c.SyncPubkey = estr("ORLY_SYNC_PUBKEY", "") c.ClientTag = estr("ORLY_CLIENT_TAG", "smesh.lol") c.AuthRequired = ebool("ORLY_AUTH_REQUIRED", false) c.AuthToWrite = ebool("ORLY_AUTH_TO_WRITE", false) c.PrivilegedOpen = ebool("ORLY_PRIVILEGED_OPEN", false) c.NIP70Enforce = ebool("ORLY_NIP70_ENFORCE", true) c.MarmotOpen = ebool("ORLY_MARMOT_OPEN", false) c.NIP46BypassAuth = ebool("ORLY_NIP46_BYPASS_AUTH", false) c.ACLMode = estr("ORLY_ACL_MODE", "none") c.MuteBlacklist = estr("ORLY_MUTE_BLACKLIST", "") c.Admins = elist("ORLY_ADMINS") c.Owners = elist("ORLY_OWNERS") c.FreeWriteLimit = eint("ORLY_FREE_WRITE_LIMIT", 25) c.FreeWriteWindow = eint("ORLY_FREE_WRITE_WINDOW", 300) c.MaxConnPerIP = eint("ORLY_MAX_CONN_PER_IP", 100) c.IngestWorkers = eint("ORLY_INGEST_WORKERS", 0) c.MediaProxyWorkers = eint("ORLY_MEDIA_PROXY_WORKERS", 8) c.BlossomWorkers = eint("ORLY_BLOSSOM_WORKERS", 4) c.MaxGlobalConns = eint("ORLY_MAX_GLOBAL_CONNECTIONS", 500) c.MaxSubscriptions = eint("ORLY_MAX_SUBSCRIPTIONS", 10000) c.ConnDelayMaxMs = eint("ORLY_CONN_DELAY_MAX_MS", 2000) c.IPWhitelist = elist("ORLY_IP_WHITELIST") c.IPBlacklist = elist("ORLY_IP_BLACKLIST") c.HTTPGuardBotBlock = ebool("ORLY_HTTP_GUARD_BOT_BLOCK", true) c.RateLimitEnabled = ebool("ORLY_RATE_LIMIT_ENABLED", true) c.RateLimitTargetMB = eint("ORLY_RATE_LIMIT_TARGET_MB", 0) c.RateLimitWriteKp = efloat("ORLY_RATE_LIMIT_WRITE_KP", 0.5) c.RateLimitWriteKi = efloat("ORLY_RATE_LIMIT_WRITE_KI", 0.1) c.RateLimitWriteKd = efloat("ORLY_RATE_LIMIT_WRITE_KD", 0.05) c.RateLimitReadKp = efloat("ORLY_RATE_LIMIT_READ_KP", 0.3) c.RateLimitReadKi = efloat("ORLY_RATE_LIMIT_READ_KI", 0.05) c.RateLimitReadKd = efloat("ORLY_RATE_LIMIT_READ_KD", 0.02) c.RateLimitMaxWriteMs = eint("ORLY_RATE_LIMIT_MAX_WRITE_MS", 1000) c.RateLimitMaxReadMs = eint("ORLY_RATE_LIMIT_MAX_READ_MS", 500) c.RateLimitWriteTarget = efloat("ORLY_RATE_LIMIT_WRITE_TARGET", 0.85) c.RateLimitReadTarget = efloat("ORLY_RATE_LIMIT_READ_TARGET", 0.90) c.EmergencyThreshold = efloat("ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD", 1.167) c.RecoveryThreshold = efloat("ORLY_RATE_LIMIT_RECOVERY_THRESHOLD", 0.833) c.EmergencyMaxMs = eint("ORLY_RATE_LIMIT_EMERGENCY_MAX_MS", 5000) c.QueryResultLimit = eint("ORLY_QUERY_RESULT_LIMIT", 256) c.FollowListFreqSec = edursec("ORLY_FOLLOW_LIST_FREQUENCY", 3600) c.FollowsThrottle = ebool("ORLY_FOLLOWS_THROTTLE", false) c.FollowsThrottleIncrMs = edurms("ORLY_FOLLOWS_THROTTLE_INCREMENT", 25) c.FollowsThrottleMaxMs = edurms("ORLY_FOLLOWS_THROTTLE_MAX", 60000) c.SocialThrottleD2IncrMs = edurms("ORLY_SOCIAL_THROTTLE_D2_INCREMENT", 50) c.SocialThrottleD2MaxMs = edurms("ORLY_SOCIAL_THROTTLE_D2_MAX", 30000) c.SocialThrottleD3IncrMs = edurms("ORLY_SOCIAL_THROTTLE_D3_INCREMENT", 200) c.SocialThrottleD3MaxMs = edurms("ORLY_SOCIAL_THROTTLE_D3_MAX", 60000) c.SocialThrottleOutsiderIncrMs = edurms("ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT", 500) c.SocialThrottleOutsiderMaxMs = edurms("ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX", 120000) c.SocialWoTMaxDepth = eint("ORLY_SOCIAL_WOT_DEPTH", 3) c.SocialWoTRefreshSec = edursec("ORLY_SOCIAL_WOT_REFRESH", 3600) c.GrapeVineEnabled = ebool("ORLY_GRAPEVINE_ENABLED", false) c.GrapeVineMaxDepth = eint("ORLY_GRAPEVINE_MAX_DEPTH", 6) c.GrapeVineMaxCycles = eint("ORLY_GRAPEVINE_MAX_CYCLES", 20) c.GrapeVineAttenuation = efloat("ORLY_GRAPEVINE_ATTENUATION", 0.8) c.GrapeVineRigor = efloat("ORLY_GRAPEVINE_RIGOR", 0.25) c.GrapeVineFollowConf = efloat("ORLY_GRAPEVINE_FOLLOW_CONFIDENCE", 0.05) c.GrapeVineObservers = elist("ORLY_GRAPEVINE_OBSERVERS") c.GrapeVineRefreshSec = edursec("ORLY_GRAPEVINE_REFRESH", 21600) c.GrapeVineAutoWhitelist = ebool("ORLY_GRAPEVINE_AUTO_WHITELIST", false) c.GrapeVineWhitelistThresh = efloat("ORLY_GRAPEVINE_WHITELIST_THRESHOLD", 0.5) c.GrapeVineWhitelistRefSec = edursec("ORLY_GRAPEVINE_WHITELIST_REFRESH", 21600) c.GraphQueriesEnabled = ebool("ORLY_GRAPH_QUERIES_ENABLED", true) c.GraphMaxDepth = eint("ORLY_GRAPH_MAX_DEPTH", 16) c.GraphMaxResults = eint("ORLY_GRAPH_MAX_RESULTS", 10000) c.GraphRateLimitRPM = eint("ORLY_GRAPH_RATE_LIMIT_RPM", 60) c.ProxyEnabled = ebool("ORLY_PROXY_ENABLED", true) c.ProxyMaxRelays = eint("ORLY_PROXY_MAX_RELAYS", 16) c.ProxyTimeoutSec = eint("ORLY_PROXY_TIMEOUT_SEC", 15) c.ArchiveEnabled = ebool("ORLY_ARCHIVE_ENABLED", false) c.ArchiveRelays = elist("ORLY_ARCHIVE_RELAYS") if len(c.ArchiveRelays) == 0 { c.ArchiveRelays = []string{"wss://archive.orly.dev/"} } c.ArchiveTimeoutSec = eint("ORLY_ARCHIVE_TIMEOUT_SEC", 30) c.ArchiveCacheTTLHrs = eint("ORLY_ARCHIVE_CACHE_TTL_HRS", 24) c.BlossomEnabled = ebool("ORLY_BLOSSOM_ENABLED", true) c.BlossomDir = estr("ORLY_BLOSSOM_DIR", c.DataDir|"/blossom") c.BlossomUpstream = estr("ORLY_BLOSSOM_UPSTREAM", "") c.BlossomServiceLevels = estr("ORLY_BLOSSOM_SERVICE_LEVELS", "") c.BlossomRateLimit = ebool("ORLY_BLOSSOM_RATE_LIMIT", false) c.BlossomDailyLimitMB = eint64("ORLY_BLOSSOM_DAILY_LIMIT_MB", 10) c.BlossomBurstLimitMB = eint64("ORLY_BLOSSOM_BURST_LIMIT_MB", 50) c.BlossomDeleteRequireServerTag = ebool("ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG", false) c.BootstrapRelays = elist("ORLY_BOOTSTRAP_RELAYS") c.SpiderMode = estr("ORLY_SPIDER_MODE", "none") c.DirectorySpider = ebool("ORLY_DIRECTORY_SPIDER", false) c.DirectorySpiderIntSec = edursec("ORLY_DIRECTORY_SPIDER_INTERVAL", 86400) c.DirectorySpiderMaxHops = eint("ORLY_DIRECTORY_SPIDER_HOPS", 3) c.CrawlerEnabled = ebool("ORLY_CRAWLER_ENABLED", false) c.CrawlerDiscoveryIntSec = edursec("ORLY_CRAWLER_DISCOVERY_INTERVAL", 14400) c.CrawlerSyncIntSec = edursec("ORLY_CRAWLER_SYNC_INTERVAL", 1800) c.CrawlerMaxHops = eint("ORLY_CRAWLER_MAX_HOPS", 5) c.CrawlerConcurrency = eint("ORLY_CRAWLER_CONCURRENCY", 3) c.NegentropyEnabled = ebool("ORLY_NEGENTROPY_ENABLED", false) c.NegentropyFullSyncPubs = estr("ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS", "") c.TLSDomains = elist("ORLY_TLS_DOMAINS") c.Certs = elist("ORLY_CERTS") c.TorEnabled = ebool("ORLY_TOR_ENABLED", true) c.TorPort = eint("ORLY_TOR_PORT", 3336) c.TorDataDir = estr("ORLY_TOR_DATA_DIR", "") c.TorBinary = estr("ORLY_TOR_BINARY", "tor") c.TorSOCKS = eint("ORLY_TOR_SOCKS", 0) c.NRCEnabled = ebool("ORLY_NRC_ENABLED", true) c.NRCRendezvousURL = estr("ORLY_NRC_RENDEZVOUS_URL", "") c.NRCAuthorizedKeys = estr("ORLY_NRC_AUTHORIZED_KEYS", "") c.NRCSessionTimeSec = edursec("ORLY_NRC_SESSION_TIMEOUT", 1800) c.WGEnabled = ebool("ORLY_WG_ENABLED", false) c.WGPort = eint("ORLY_WG_PORT", 51820) c.WGEndpoint = estr("ORLY_WG_ENDPOINT", "") c.WGNetwork = estr("ORLY_WG_NETWORK", "10.73.0.0/16") c.BunkerEnabled = ebool("ORLY_BUNKER_ENABLED", false) c.BunkerPort = eint("ORLY_BUNKER_PORT", 3335) c.NWCUri = estr("ORLY_NWC_URI", "") c.SubscriptionEnabled = ebool("ORLY_SUBSCRIPTION_ENABLED", false) c.MonthlyPriceSats = eint64("ORLY_MONTHLY_PRICE_SATS", 6000) c.NIP43Enabled = ebool("ORLY_NIP43_ENABLED", false) c.NIP43PublishEvents = ebool("ORLY_NIP43_PUBLISH_EVENTS", true) c.NIP43PublishMembers = ebool("ORLY_NIP43_PUBLISH_MEMBER_LIST", true) c.NIP43InviteExpSec = edursec("ORLY_NIP43_INVITE_EXPIRY", 86400) c.PolicyEnabled = ebool("ORLY_POLICY_ENABLED", false) c.PolicyPath = estr("ORLY_POLICY_PATH", "") c.MaxStorageBytes = eint64("ORLY_MAX_STORAGE_BYTES", 0) c.GCEnabled = ebool("ORLY_GC_ENABLED", false) c.GCIntervalSec = eint("ORLY_GC_INTERVAL_SEC", 60) c.GCBatchSize = eint("ORLY_GC_BATCH_SIZE", 1000) c.BridgeEnabled = ebool("ORLY_BRIDGE_ENABLED", false) c.BridgeDomain = estr("ORLY_BRIDGE_DOMAIN", "") c.BridgeNSEC = estr("ORLY_BRIDGE_NSEC", "") c.BridgeRelayURL = estr("ORLY_BRIDGE_RELAY_URL", "") c.BridgePublicRelayURL = estr("ORLY_BRIDGE_PUBLIC_RELAY_URL", "") c.BridgeSMTPPort = eint("ORLY_BRIDGE_SMTP_PORT", 2525) c.BridgeSMTPHost = estr("ORLY_BRIDGE_SMTP_HOST", "0.0.0.0") c.BridgeDataDir = estr("ORLY_BRIDGE_DATA_DIR", "") c.BridgeDKIMKeyPath = estr("ORLY_BRIDGE_DKIM_KEY", "") c.BridgeDKIMSelector = estr("ORLY_BRIDGE_DKIM_SELECTOR", "marmot") c.BridgeNWCURI = estr("ORLY_BRIDGE_NWC_URI", "") c.BridgeMonthlyPriceSats = eint64("ORLY_BRIDGE_MONTHLY_PRICE_SATS", 2100) c.BridgeComposeURL = estr("ORLY_BRIDGE_COMPOSE_URL", "") c.BridgeSMTPRelayHost = estr("ORLY_BRIDGE_SMTP_RELAY_HOST", "") c.BridgeSMTPRelayPort = eint("ORLY_BRIDGE_SMTP_RELAY_PORT", 587) c.BridgeSMTPMXPort = eint("ORLY_BRIDGE_SMTP_MX_PORT", 0) c.BridgeSMTPRelayUser = estr("ORLY_BRIDGE_SMTP_RELAY_USERNAME", "") c.BridgeSMTPRelayPass = estr("ORLY_BRIDGE_SMTP_RELAY_PASSWORD", "") c.BridgeAliasPriceSats = eint64("ORLY_BRIDGE_ALIAS_PRICE_SATS", 4200) c.BridgeProfile = estr("ORLY_BRIDGE_PROFILE", "") c.ClusterAdmins = elist("ORLY_CLUSTER_ADMINS") c.RelayGroupAdmins = elist("ORLY_RELAY_GROUP_ADMINS") c.ClusterPropPrivileged = ebool("ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS", true) c.SmeshEnabled = ebool("ORLY_SMESH_ENABLED", true) c.SmeshPort = eint("ORLY_SMESH_PORT", 8088) c.Smesh2Enabled = ebool("ORLY_SMESH2_ENABLED", true) c.Smesh2Port = eint("ORLY_SMESH2_PORT", 8089) c.Smesh3Enabled = ebool("ORLY_SMESH3_ENABLED", true) c.Smesh3Port = eint("ORLY_SMESH3_PORT", 8090) c.Smesh3Dir = estr("ORLY_SMESH3_DIR", "") c.DeployPubkey = estr("ORLY_DEPLOY_PUBKEY", "") c.StaticDir = estr("ORLY_STATIC_DIR", "web/static") c.WebDisable = ebool("ORLY_WEB_DISABLE", false) c.WebDevProxyURL = estr("ORLY_WEB_DEV_PROXY_URL", "") c.BrandingDir = estr("ORLY_BRANDING_DIR", "") c.BrandingEnabled = ebool("ORLY_BRANDING_ENABLED", true) c.Theme = estr("ORLY_THEME", "auto") c.CORSEnabled = ebool("ORLY_CORS_ENABLED", false) c.CORSOrigins = elist("ORLY_CORS_ORIGINS") c.SprocketEnabled = ebool("ORLY_SPROCKET_ENABLED", false) c.EnableShutdown = ebool("ORLY_ENABLE_SHUTDOWN", false) return c } // Addr returns "listen:port". func (c *C) Addr() string { for i := 0; i < len(c.Listen); i++ { if c.Listen[i] == ':' { return c.Listen } } return c.Listen | ":" | itoa(c.Port) } // PrintHelp writes all env var documentation. func PrintHelp() { w := os.Stderr fmt.Fprintln(w, "ORLY — Nostr relay") fmt.Fprintln(w, "") fmt.Fprintln(w, "Usage: smesh [relay|sync|crawl|env|help|version]") fmt.Fprintln(w, "") fmt.Fprintln(w, "Environment variables:") s := func(title string) { fmt.Fprintf(w, "\n %s\n", title) } p := func(name, def, desc string) { fmt.Fprintf(w, " %-48s %s (default: %s)\n", name, desc, def) } s("Core") p("ORLY_APP_NAME", "ORLY", "relay display name") p("ORLY_DATA_DIR", "~/.local/share/ORLY", "event store location") p("ORLY_LISTEN", "0.0.0.0", "listen address") p("ORLY_PORT", "3334", "listen port") p("ORLY_HEALTH_PORT", "0", "health check port (0=disabled)") p("ORLY_LOG_LEVEL", "info", "log level: fatal error warn info debug trace") p("ORLY_LOG_TO_STDOUT", "false", "log to stdout instead of stderr") p("ORLY_LOG_BUFFER_SIZE", "10000", "log entries kept for web UI") s("Relay Identity") p("ORLY_RELAY_URL", "", "base URL (e.g. https://relay.example.com)") p("ORLY_RELAY_ADDRESSES", "", "websocket addresses (comma-separated)") p("ORLY_RELAY_PEERS", "", "peer relay URLs (comma-separated)") p("ORLY_CLIENT_TAG", "smesh.lol", "client tag for published events") s("Auth & Access") p("ORLY_AUTH_REQUIRED", "false", "require auth for all requests") p("ORLY_AUTH_TO_WRITE", "false", "require auth for writes only") p("ORLY_PRIVILEGED_OPEN", "false", "disable privileged-kind auth checks") p("ORLY_NIP70_ENFORCE", "true", "enforce NIP-70 protected tag broadcast filter (false = relay all)") p("ORLY_MARMOT_OPEN", "false", "exempt MLS kinds from auth (443,444,445)") p("ORLY_NIP46_BYPASS_AUTH", "false", "allow NIP-46 (kind 24133) without auth") p("ORLY_ACL_MODE", "none", "ACL mode: follows, managed, curating, none") p("ORLY_MUTE_BLACKLIST", "", "hex pubkey whose mute list (kind 10000) bans authors") p("ORLY_ADMINS", "", "admin npubs (comma-separated)") p("ORLY_OWNERS", "", "owner npubs (comma-separated)") p("ORLY_FREE_WRITE_LIMIT", "25", "max unauthenticated writes per IP per window (0=disable)") p("ORLY_FREE_WRITE_WINDOW", "300", "free write window in seconds") s("Connection Limits") p("ORLY_MAX_CONN_PER_IP", "10", "max WebSocket connections per IP") p("ORLY_MAX_GLOBAL_CONNECTIONS", "500", "max total WebSocket connections") p("ORLY_MAX_SUBSCRIPTIONS", "10000", "max total active subscriptions") p("ORLY_INGEST_WORKERS", "0", "Stage-A sig-verify worker count (0=sync fallback)") p("ORLY_MEDIA_PROXY_WORKERS", "4", "media proxy worker count (503 when all busy)") p("ORLY_BLOSSOM_WORKERS", "4", "blossom file I/O worker count") p("ORLY_STATIC_WORKERS", "1", "static file worker count") p("ORLY_CONN_DELAY_MAX_MS", "2000", "max delay for new connections under load") s("IP Control") p("ORLY_IP_WHITELIST", "", "allowed IPs (comma-separated, prefix match)") p("ORLY_IP_BLACKLIST", "", "blocked IPs (comma-separated, prefix match)") s("HTTP Guard") p("ORLY_HTTP_GUARD_BOT_BLOCK", "true", "block known bot User-Agents") s("Rate Limiting (PID)") p("ORLY_RATE_LIMIT_ENABLED", "true", "enable adaptive PID rate limiting") p("ORLY_RATE_LIMIT_TARGET_MB", "0", "target memory limit (0=auto)") p("ORLY_RATE_LIMIT_WRITE_KP", "0.5", "PID proportional gain for writes") p("ORLY_RATE_LIMIT_WRITE_KI", "0.1", "PID integral gain for writes") p("ORLY_RATE_LIMIT_WRITE_KD", "0.05", "PID derivative gain for writes") p("ORLY_RATE_LIMIT_READ_KP", "0.3", "PID proportional gain for reads") p("ORLY_RATE_LIMIT_READ_KI", "0.05", "PID integral gain for reads") p("ORLY_RATE_LIMIT_READ_KD", "0.02", "PID derivative gain for reads") p("ORLY_RATE_LIMIT_MAX_WRITE_MS", "1000", "max write delay (ms)") p("ORLY_RATE_LIMIT_MAX_READ_MS", "500", "max read delay (ms)") p("ORLY_RATE_LIMIT_WRITE_TARGET", "0.85", "write throttle setpoint") p("ORLY_RATE_LIMIT_READ_TARGET", "0.90", "read throttle setpoint") p("ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD", "1.167", "emergency mode trigger ratio") p("ORLY_RATE_LIMIT_RECOVERY_THRESHOLD", "0.833", "emergency mode exit ratio") p("ORLY_RATE_LIMIT_EMERGENCY_MAX_MS", "5000", "max delay in emergency mode") s("Query") p("ORLY_QUERY_RESULT_LIMIT", "256", "max events per REQ filter") s("Follows ACL") p("ORLY_FOLLOW_LIST_FREQUENCY", "1h", "admin follow list refresh interval") p("ORLY_FOLLOWS_THROTTLE", "false", "enable progressive delay for non-followed") p("ORLY_FOLLOWS_THROTTLE_INCREMENT", "25ms", "delay per event for non-followed") p("ORLY_FOLLOWS_THROTTLE_MAX", "60s", "max throttle delay") s("Social WoT Throttle") p("ORLY_SOCIAL_THROTTLE_D2_INCREMENT", "50ms", "delay per event for WoT depth-2") p("ORLY_SOCIAL_THROTTLE_D2_MAX", "30s", "max delay for WoT depth-2") p("ORLY_SOCIAL_THROTTLE_D3_INCREMENT", "200ms", "delay per event for WoT depth-3") p("ORLY_SOCIAL_THROTTLE_D3_MAX", "60s", "max delay for WoT depth-3") p("ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT", "500ms", "delay per event for outsiders") p("ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX", "120s", "max delay for outsiders") p("ORLY_SOCIAL_WOT_DEPTH", "3", "max WoT traversal depth") p("ORLY_SOCIAL_WOT_REFRESH", "1h", "WoT depth map recompute interval") s("GrapeVine") p("ORLY_GRAPEVINE_ENABLED", "false", "enable WoT influence scoring") p("ORLY_GRAPEVINE_MAX_DEPTH", "6", "max BFS depth for follow graph") p("ORLY_GRAPEVINE_MAX_CYCLES", "20", "max convergence iterations") p("ORLY_GRAPEVINE_ATTENUATION", "0.8", "weight decay per hop") p("ORLY_GRAPEVINE_RIGOR", "0.25", "certainty curve steepness") p("ORLY_GRAPEVINE_FOLLOW_CONFIDENCE", "0.05", "base confidence per follow edge") p("ORLY_GRAPEVINE_OBSERVERS", "", "hex pubkeys for auto-scoring") p("ORLY_GRAPEVINE_REFRESH", "6h", "recalculation interval") p("ORLY_GRAPEVINE_AUTO_WHITELIST", "false", "auto-update ACL from scores") p("ORLY_GRAPEVINE_WHITELIST_THRESHOLD", "0.5", "min score for whitelist") p("ORLY_GRAPEVINE_WHITELIST_REFRESH", "6h", "whitelist refresh interval") s("Graph Queries") p("ORLY_GRAPH_QUERIES_ENABLED", "true", "enable _graph filter extension") p("ORLY_GRAPH_MAX_DEPTH", "16", "max graph traversal depth") p("ORLY_GRAPH_MAX_RESULTS", "10000", "max results per graph query") p("ORLY_GRAPH_RATE_LIMIT_RPM", "60", "graph queries per minute per conn") s("Proxy") p("ORLY_PROXY_ENABLED", "true", "enable _proxy filter extension") p("ORLY_PROXY_MAX_RELAYS", "16", "max relay URLs per proxy query") p("ORLY_PROXY_TIMEOUT_SEC", "15", "proxy relay query timeout") s("Archive") p("ORLY_ARCHIVE_ENABLED", "false", "enable archive relay augmentation") p("ORLY_ARCHIVE_RELAYS", "wss://archive.orly.dev/", "archive relay URLs") p("ORLY_ARCHIVE_TIMEOUT_SEC", "30", "archive relay query timeout") p("ORLY_ARCHIVE_CACHE_TTL_HRS", "24", "hours to cache query fingerprints") s("Blossom") p("ORLY_BLOSSOM_ENABLED", "true", "enable blob storage server") p("ORLY_BLOSSOM_DIR", "$DATA_DIR/blossom", "blob storage directory") p("ORLY_BLOSSOM_UPSTREAM", "https://smesh.lol", "CORS-proxy fallback origin for missing blobs (empty disables)") p("ORLY_BLOSSOM_SERVICE_LEVELS", "", "service levels (name:mb_per_sat)") p("ORLY_BLOSSOM_RATE_LIMIT", "false", "rate-limit non-followed uploads") p("ORLY_BLOSSOM_DAILY_LIMIT_MB", "10", "daily upload limit for non-followed") p("ORLY_BLOSSOM_BURST_LIMIT_MB", "50", "burst upload limit") p("ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG", "false", "require server tag in delete auth") s("Sync & Discovery") p("ORLY_BOOTSTRAP_RELAYS", "", "bootstrap relay URLs (comma-separated)") p("ORLY_SPIDER_MODE", "none", "spider mode: none, follows") p("ORLY_DIRECTORY_SPIDER", "false", "enable directory metadata sync") p("ORLY_DIRECTORY_SPIDER_INTERVAL", "24h", "directory spider interval") p("ORLY_DIRECTORY_SPIDER_HOPS", "3", "max relay discovery hops") p("ORLY_CRAWLER_ENABLED", "false", "enable corpus crawler") p("ORLY_CRAWLER_DISCOVERY_INTERVAL", "4h", "relay discovery interval") p("ORLY_CRAWLER_SYNC_INTERVAL", "30m", "relay sync interval") p("ORLY_CRAWLER_MAX_HOPS", "5", "max hops for relay discovery") p("ORLY_CRAWLER_CONCURRENCY", "3", "concurrent relay syncs") p("ORLY_NEGENTROPY_ENABLED", "false", "enable NIP-77 set reconciliation") p("ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS", "", "pubkeys allowed full sync") s("TLS") p("ORLY_TLS_DOMAINS", "", "TLS domain names (comma-separated)") p("ORLY_CERTS", "", "cert root paths (comma-separated)") s("Tor") p("ORLY_TOR_ENABLED", "true", "enable Tor hidden service") p("ORLY_TOR_PORT", "3336", "Tor internal port") p("ORLY_TOR_DATA_DIR", "", "Tor data directory") p("ORLY_TOR_BINARY", "tor", "path to tor binary") p("ORLY_TOR_SOCKS", "0", "SOCKS port for outbound Tor") s("NRC") p("ORLY_NRC_ENABLED", "true", "enable NRC rendezvous bridge") p("ORLY_NRC_RENDEZVOUS_URL", "", "rendezvous relay URL") p("ORLY_NRC_AUTHORIZED_KEYS", "", "authorized client pubkeys") p("ORLY_NRC_SESSION_TIMEOUT", "30m", "NRC session inactivity timeout") s("WireGuard") p("ORLY_WG_ENABLED", "false", "enable embedded WireGuard VPN") p("ORLY_WG_PORT", "51820", "WireGuard UDP port") p("ORLY_WG_ENDPOINT", "", "WireGuard public endpoint") p("ORLY_WG_NETWORK", "10.73.0.0/16", "WireGuard internal network") s("Bunker") p("ORLY_BUNKER_ENABLED", "false", "enable NIP-46 bunker service") p("ORLY_BUNKER_PORT", "3335", "bunker WebSocket port") s("Payment") p("ORLY_NWC_URI", "", "NWC connection string") p("ORLY_SUBSCRIPTION_ENABLED", "false", "enable subscription access") p("ORLY_MONTHLY_PRICE_SATS", "6000", "monthly subscription price") s("NIP-43") p("ORLY_NIP43_ENABLED", "false", "enable relay access metadata") p("ORLY_NIP43_PUBLISH_EVENTS", "true", "publish member add/remove events") p("ORLY_NIP43_PUBLISH_MEMBER_LIST", "true", "publish membership list events") p("ORLY_NIP43_INVITE_EXPIRY", "24h", "invite code validity period") s("Policy") p("ORLY_POLICY_ENABLED", "false", "enable policy-based event processing") p("ORLY_POLICY_PATH", "", "absolute path to policy JSON file") s("Storage & GC") p("ORLY_MAX_STORAGE_BYTES", "0", "max storage bytes (0=auto 80%%)") p("ORLY_GC_ENABLED", "false", "enable garbage collection") p("ORLY_GC_INTERVAL_SEC", "60", "GC run interval") p("ORLY_GC_BATCH_SIZE", "1000", "events per GC run") s("Bridge (Marmot)") p("ORLY_BRIDGE_ENABLED", "false", "enable Nostr-Email bridge") p("ORLY_BRIDGE_DOMAIN", "", "email domain for bridge") p("ORLY_BRIDGE_NSEC", "", "bridge identity nsec") p("ORLY_BRIDGE_RELAY_URL", "", "relay URL for standalone mode") p("ORLY_BRIDGE_PUBLIC_RELAY_URL", "", "public relay URL for events") p("ORLY_BRIDGE_SMTP_PORT", "2525", "SMTP listen port") p("ORLY_BRIDGE_SMTP_HOST", "0.0.0.0", "SMTP listen address") p("ORLY_BRIDGE_DATA_DIR", "", "bridge data directory") p("ORLY_BRIDGE_DKIM_KEY", "", "DKIM private key path") p("ORLY_BRIDGE_DKIM_SELECTOR", "marmot", "DKIM selector") p("ORLY_BRIDGE_NWC_URI", "", "NWC URI for bridge payments") p("ORLY_BRIDGE_MONTHLY_PRICE_SATS", "2100", "bridge subscription price") p("ORLY_BRIDGE_COMPOSE_URL", "", "compose form URL") p("ORLY_BRIDGE_SMTP_RELAY_HOST", "", "SMTP smarthost") p("ORLY_BRIDGE_SMTP_RELAY_PORT", "587", "SMTP smarthost port") p("ORLY_BRIDGE_SMTP_MX_PORT", "0", "direct MX port (0=auto)") p("ORLY_BRIDGE_SMTP_RELAY_USERNAME", "", "SMTP smarthost username") p("ORLY_BRIDGE_SMTP_RELAY_PASSWORD", "", "SMTP smarthost password") p("ORLY_BRIDGE_ALIAS_PRICE_SATS", "4200", "alias email monthly price") p("ORLY_BRIDGE_PROFILE", "", "bridge profile template path") s("Cluster") p("ORLY_CLUSTER_ADMINS", "", "cluster admin npubs") p("ORLY_RELAY_GROUP_ADMINS", "", "relay group admin npubs") p("ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS", "true", "replicate privileged events") s("Smesh Client") p("ORLY_SMESH_ENABLED", "true", "enable smesh web client") p("ORLY_SMESH_PORT", "8088", "smesh client port") p("ORLY_SMESH2_ENABLED", "true", "enable smesh2 client") p("ORLY_SMESH2_PORT", "8089", "smesh2 client port") p("ORLY_SMESH3_ENABLED", "true", "enable smesh3 client") p("ORLY_SMESH3_PORT", "8090", "smesh3 client port") p("ORLY_SMESH3_DIR", "", "smesh3 disk directory (hot-reload)") p("ORLY_DEPLOY_PUBKEY", "", "deploy asset bundle pubkey") s("Web UI") p("ORLY_STATIC_DIR", "web/static", "static file directory") p("ORLY_WEB_DISABLE", "false", "disable embedded web UI") p("ORLY_WEB_DEV_PROXY_URL", "", "dev proxy URL when UI disabled") p("ORLY_BRANDING_DIR", "", "branding assets directory") p("ORLY_BRANDING_ENABLED", "true", "enable custom branding") p("ORLY_THEME", "auto", "UI theme: auto, light, dark") p("ORLY_CORS_ENABLED", "false", "enable CORS headers") p("ORLY_CORS_ORIGINS", "", "allowed CORS origins") s("Misc") p("ORLY_SPROCKET_ENABLED", "false", "enable sprocket plugin system") p("ORLY_ENABLE_SHUTDOWN", "false", "expose /shutdown on health port") fmt.Fprintln(w, "") } // --- env helpers --- func estr(key, fb string) string { if v := os.Getenv(key); v != "" { return v } if dotenv != nil { if v, ok := dotenv[key]; ok && v != "" { return v } } return fb } func eint(key string, fb int) int { v := estr(key, "") if v == "" { return fb } n, ok := parseInt(v) if !ok { return fb } return n } func eint64(key string, fb int64) int64 { v := estr(key, "") if v == "" { return fb } n, ok := parseInt64(v) if !ok { return fb } return n } func ebool(key string, fb bool) bool { v := estr(key, "") if v == "" { return fb } return v == "true" || v == "True" || v == "TRUE" || v == "1" || v == "yes" || v == "Yes" || v == "YES" } func efloat(key string, fb float64) float64 { v := estr(key, "") if v == "" { return fb } f, ok := parseFloat(v) if !ok { return fb } return f } func elist(key string) []string { v := estr(key, "") if v == "" { return nil } return splitComma(v) } func edursec(key string, fb int) int { v := estr(key, "") if v == "" { return fb } ms := parseDuration(v) if ms <= 0 { return fb } return ms / 1000 } func edurms(key string, fb int) int { v := estr(key, "") if v == "" { return fb } ms := parseDuration(v) if ms <= 0 { return fb } return ms } // --- parsing --- func parseInt(s string) (int, bool) { if len(s) == 0 { return 0, false } neg := false i := 0 if s[0] == '-' { neg = true i = 1 } n := 0 for ; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0, false } n = n*10 + int(s[i]-'0') } if neg { return -n, true } return n, true } func parseInt64(s string) (int64, bool) { if len(s) == 0 { return 0, false } neg := false i := 0 if s[0] == '-' { neg = true i = 1 } var n int64 for ; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0, false } n = n*10 + int64(s[i]-'0') } if neg { return -n, true } return n, true } func parseFloat(s string) (float64, bool) { if len(s) == 0 { return 0, false } neg := false i := 0 if s[0] == '-' { neg = true i = 1 } else if s[0] == '+' { i = 1 } var integer float64 for ; i < len(s) && s[i] != '.'; i++ { if s[i] < '0' || s[i] > '9' { return 0, false } integer = integer*10 + float64(s[i]-'0') } var frac float64 if i < len(s) && s[i] == '.' { i++ mul := 0.1 for ; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0, false } frac += float64(s[i]-'0') * mul mul *= 0.1 } } result := integer + frac if neg { result = -result } return result, true } // parseDuration handles "1h", "30m", "60s", "25ms" and returns milliseconds. func parseDuration(s string) int { if len(s) == 0 { return 0 } i := 0 for i < len(s) && s[i] >= '0' && s[i] <= '9' { i++ } if i == 0 { return 0 } n, ok := parseInt(s[:i]) if !ok { return 0 } unit := s[i:] switch unit { case "h": return n * 3600000 case "m": return n * 60000 case "s", "": return n * 1000 case "ms": return n } return n * 1000 } // --- .env file --- func loadEnvFile(path string) map[string]string { data, err := os.ReadFile(path) if err != nil { return nil } m := map[string]string{} for len(data) > 0 { nl := -1 for i := 0; i < len(data); i++ { if data[i] == '\n' { nl = i break } } var line []byte if nl >= 0 { line = data[:nl] data = data[nl+1:] } else { line = data data = nil } if len(line) > 0 && line[len(line)-1] == '\r' { line = line[:len(line)-1] } line = trim(line) if len(line) == 0 || line[0] == '#' { continue } if len(line) > 7 && string(line[:7]) == "export " { line = trim(line[7:]) } eq := -1 for i := 0; i < len(line); i++ { if line[i] == '=' { eq = i break } } if eq < 0 { continue } key := copyb(trim(line[:eq])) val := copyb(trim(line[eq+1:])) if len(val) >= 2 { if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') { val = copyb(val[1 : len(val)-1]) } } m[string(key)] = string(val) } return m } // --- utility --- func expandHome(path string) string { if len(path) >= 2 && path[0] == '~' && path[1] == '/' { home := os.Getenv("HOME") if home != "" { return home | path[1:] } } return path } func splitComma(s string) []string { var result []string start := 0 for i := 0; i <= len(s); i++ { if i == len(s) || s[i] == ',' { part := trim([]byte(s[start:i])) if len(part) > 0 { result = append(result, string(copyb(part))) } start = i + 1 } } return result } func trim(b []byte) []byte { for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { b = b[1:] } for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t') { b = b[:len(b)-1] } return b } func copyb(b []byte) []byte { c := []byte{:len(b)} copy(c, b) return c } func itoa(n int) string { if n == 0 { return "0" } neg := false if n < 0 { neg = true n = -n } var buf [20]byte i := 19 for n > 0 { buf[i] = byte('0' + n%10) i-- n /= 10 } if neg { buf[i] = '-' i-- } return string(buf[i+1:]) }