// Package config loads torly's settings from the environment. // // Every variable has a TORLY_ name and an ORLY_ fallback, so an existing orly // or morly deployment can be pointed at torly by changing only the binary. package config import ( "fmt" "os" "path/filepath" "strconv" "strings" ) // Version is the relay version, set by the build. var Version = "0.1.0" // Config is the complete runtime configuration. type Config struct { AppName string Description string Contact string Icon string ListenHost string Port int DataDir string StaticDir string RelayURL string SecretKey string // RelayPubKey is derived at startup from SecretKey or the persisted relay // key file. It is not read from the environment directly. RelayPubKey string AuthRequired bool MaxConnections int ConnWorkers int VerifyWorkers int BlossomEnabled bool BlossomMaxBlob int64 MaxEventBytes int MaxSubsPerConn int MaxFilters int MaxLimit int QueryLimit int MaxFutureSecs int64 ExpiryInterval int64 // seconds between NIP-40 purges LogLevel string } // FromEnv builds a Config, applying defaults for absent variables. func FromEnv() (*Config, error) { home, _ := os.UserHomeDir() if home == "" { home = "." } c := &Config{ AppName: env("APP_NAME", "torly"), Description: env("DESCRIPTION", "torly - a TinyGo Nostr relay"), Contact: env("CONTACT", ""), Icon: env("ICON", ""), ListenHost: env("LISTEN", "127.0.0.1"), DataDir: env("DATA_DIR", filepath.Join(home, ".local", "share", "torly")), StaticDir: env("STATIC_DIR", ""), SecretKey: env("SECRET_KEY", ""), AuthRequired: envBool("AUTH_REQUIRED", false), MaxConnections: envInt("MAX_CONNECTIONS", 0), ConnWorkers: envInt("CONN_WORKERS", 64), VerifyWorkers: envInt("VERIFY_WORKERS", 2), BlossomEnabled: envBool("BLOSSOM_ENABLED", true), BlossomMaxBlob: int64(envInt("BLOSSOM_MAX_BLOB_MB", 50)) << 20, MaxEventBytes: envInt("MAX_EVENT_BYTES", 128<<10), MaxSubsPerConn: envInt("MAX_SUBS_PER_CONN", 64), MaxFilters: envInt("MAX_FILTERS", 16), MaxLimit: envInt("MAX_LIMIT", 5000), QueryLimit: envInt("QUERY_LIMIT", 5000), MaxFutureSecs: int64(envInt("MAX_FUTURE_SECS", 900)), ExpiryInterval: int64(envInt("EXPIRY_INTERVAL", 60)), LogLevel: strings.ToLower(env("LOG_LEVEL", "info")), } port, err := strconv.Atoi(env("PORT", "3334")) if err != nil || port < 0 || port > 65535 { return nil, fmt.Errorf("torly: invalid TORLY_PORT") } c.Port = port if c.MaxEventBytes < 1024 { return nil, fmt.Errorf("torly: TORLY_MAX_EVENT_BYTES must be at least 1024") } if c.BlossomMaxBlob < 1<<20 { c.BlossomMaxBlob = 1 << 20 } if c.MaxFilters < 1 { c.MaxFilters = 1 } if c.MaxSubsPerConn < 1 { c.MaxSubsPerConn = 1 } if c.ConnWorkers < 1 { c.ConnWorkers = 1 } if c.ConnWorkers > 1024 { c.ConnWorkers = 1024 } if c.VerifyWorkers < 1 { c.VerifyWorkers = 1 } if c.VerifyWorkers > 64 { c.VerifyWorkers = 64 } if c.MaxLimit < 1 { c.MaxLimit = 1 } if c.RelayURL = env("RELAY_URL", ""); c.RelayURL == "" { host := c.ListenHost if host == "" || host == "0.0.0.0" || host == "::" { host = "localhost" } c.RelayURL = fmt.Sprintf("ws://%s:%d/", host, c.Port) } return c, nil } // Addr returns the listen address. func (c *Config) Addr() string { return fmt.Sprintf("%s:%d", c.ListenHost, c.Port) } // NIP11 is the relay information document served for NIP-11. type NIP11 struct { Name string `json:"name"` Description string `json:"description"` PubKey string `json:"pubkey,omitempty"` Contact string `json:"contact,omitempty"` SupportedNIPs []int `json:"supported_nips"` Software string `json:"software"` Version string `json:"version"` Icon string `json:"icon,omitempty"` Limitation LimitDoc `json:"limitation"` } // LimitDoc is the NIP-11 limitation object. type LimitDoc struct { MaxMessageLength int `json:"max_message_length"` MaxSubscriptions int `json:"max_subscriptions"` MaxFilters int `json:"max_filters"` MaxLimit int `json:"max_limit"` MaxSubIDLength int `json:"max_subid_length"` MinPrefix int `json:"min_prefix"` MaxEventTags int `json:"max_event_tags"` MaxContentLength int `json:"max_content_length"` AuthRequired bool `json:"auth_required"` PaymentRequired bool `json:"payment_required"` } // SupportedNIPs lists the NIPs torly implements. var SupportedNIPs = []int{1, 9, 11, 33, 40, 42, 45, 50, 70} // EnvHelp documents the environment. func EnvHelp() string { return `torly - a TinyGo Nostr relay Usage: torly [serve] run the relay (default) torly import [opts] import newline-delimited NIP-01 events torly reindex-graph rebuild derived graph indexes from the store torly blossom-backup --data-dir DIR --out FILE torly blossom-restore --data-dir DIR --file FILE [--offline] torly version print the version torly help print this help Configuration is by environment variable. TORLY_* is authoritative; the same name with ORLY_* is accepted as a fallback. TORLY_APP_NAME NIP-11 name (default torly) TORLY_DESCRIPTION NIP-11 description TORLY_CONTACT NIP-11 contact TORLY_ICON NIP-11 icon URL TORLY_LISTEN bind address (default 127.0.0.1) TORLY_PORT bind port (default 3334) TORLY_DATA_DIR database and blob directory TORLY_STATIC_DIR optional static web root TORLY_RELAY_URL public URL used in NIP-42 checks TORLY_SECRET_KEY relay secret key (64 hex); default is DATA_DIR/relay.key TORLY_AUTH_REQUIRED require NIP-42 AUTH for writes (default false) TORLY_MAX_CONNECTIONS accepted connections cap, 0 = unlimited TORLY_CONN_WORKERS fixed reader/writer worker pairs (default 64) TORLY_VERIFY_WORKERS signature verification domains (default 2) TORLY_BLOSSOM_ENABLED serve Blossom blobs (default true) TORLY_BLOSSOM_MAX_BLOB_MB largest accepted blob in MiB (default 50) TORLY_MAX_EVENT_BYTES largest event JSON in bytes (default 131072) TORLY_MAX_SUBS_PER_CONN subscriptions per connection (default 64) TORLY_MAX_FILTERS filters per REQ (default 16) TORLY_MAX_LIMIT largest accepted filter limit (default 5000) TORLY_QUERY_LIMIT result cap for unbounded filters (default 5000) TORLY_MAX_FUTURE_SECS allowed created_at skew into the future (default 900) TORLY_EXPIRY_INTERVAL seconds between NIP-40 purges (default 60) TORLY_LOG_LEVEL error, warn, info, debug (default info) ` } func env(name, def string) string { if v := os.Getenv("TORLY_" + name); v != "" { return v } if v := os.Getenv("ORLY_" + name); v != "" { return v } return def } func envInt(name string, def int) int { v := env(name, "") if v == "" { return def } n, err := strconv.Atoi(v) if err != nil { return def } return n } func envBool(name string, def bool) bool { v := strings.ToLower(env(name, "")) switch v { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false } return def }