main.go raw

   1  // orly-acl-managed is a standalone gRPC ACL server using the Managed mode.
   2  // It provides fine-grained control via NIP-86 management API.
   3  package main
   4  
   5  import (
   6  	"context"
   7  	"os"
   8  	"path/filepath"
   9  	"strings"
  10  	"time"
  11  
  12  	"go-simpler.org/env"
  13  	"next.orly.dev/pkg/lol"
  14  	"next.orly.dev/pkg/lol/chk"
  15  	"next.orly.dev/pkg/lol/log"
  16  
  17  	"next.orly.dev/pkg/acl/server"
  18  	"next.orly.dev/pkg/database"
  19  )
  20  
  21  // Config holds the ACL server configuration.
  22  type Config struct {
  23  	// Listen is the gRPC server listen address
  24  	Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
  25  
  26  	// LogLevel is the logging level
  27  	LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
  28  
  29  	// Database configuration - Managed mode requires direct Badger access
  30  	DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory"`
  31  
  32  	// Badger configuration
  33  	BlockCacheMB        int           `env:"ORLY_DB_BLOCK_CACHE_MB" default:"256" usage:"block cache size in MB"`
  34  	IndexCacheMB        int           `env:"ORLY_DB_INDEX_CACHE_MB" default:"128" usage:"index cache size in MB"`
  35  	ZSTDLevel           int           `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
  36  	QueryCacheSizeMB    int           `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"64" usage:"query cache size in MB"`
  37  	QueryCacheMaxAge    time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
  38  	QueryCacheDisabled  bool          `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
  39  	SerialCachePubkeys  int           `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
  40  	SerialCacheEventIds int           `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
  41  
  42  	// ACL configuration
  43  	Owners         string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
  44  	Admins         string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
  45  	RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
  46  }
  47  
  48  func main() {
  49  	cfg := loadConfig()
  50  
  51  	// Set log level
  52  	lol.SetLogLevel(cfg.LogLevel)
  53  	log.I.F("orly-acl-managed starting with log level: %s", cfg.LogLevel)
  54  
  55  	ctx, cancel := context.WithCancel(context.Background())
  56  	defer cancel()
  57  
  58  	// Managed mode requires direct Badger database access (not gRPC)
  59  	dbCfg := &database.DatabaseConfig{
  60  		DataDir:             cfg.DataDir,
  61  		LogLevel:            cfg.LogLevel,
  62  		BlockCacheMB:        cfg.BlockCacheMB,
  63  		IndexCacheMB:        cfg.IndexCacheMB,
  64  		QueryCacheSizeMB:    cfg.QueryCacheSizeMB,
  65  		QueryCacheMaxAge:    cfg.QueryCacheMaxAge,
  66  		QueryCacheDisabled:  cfg.QueryCacheDisabled,
  67  		SerialCachePubkeys:  cfg.SerialCachePubkeys,
  68  		SerialCacheEventIds: cfg.SerialCacheEventIds,
  69  		ZSTDLevel:           cfg.ZSTDLevel,
  70  	}
  71  
  72  	log.I.F("initializing Badger database at %s", cfg.DataDir)
  73  	db, err := database.NewWithConfig(ctx, cancel, dbCfg)
  74  	if chk.E(err) {
  75  		log.E.F("failed to initialize database: %v", err)
  76  		os.Exit(1)
  77  	}
  78  
  79  	// Wait for database to be ready
  80  	log.I.F("waiting for database to be ready...")
  81  	<-db.Ready()
  82  	log.I.F("database ready")
  83  
  84  	// Create server config
  85  	serverCfg := &server.Config{
  86  		Listen:         cfg.Listen,
  87  		ACLMode:        "managed", // Hardcoded for this binary
  88  		LogLevel:       cfg.LogLevel,
  89  		Owners:         splitList(cfg.Owners),
  90  		Admins:         splitList(cfg.Admins),
  91  		RelayAddresses: splitList(cfg.RelayAddresses),
  92  	}
  93  
  94  	// Create and configure server
  95  	srv := server.New(db, serverCfg, true)
  96  	if err := srv.ConfigureACL(ctx); chk.E(err) {
  97  		log.E.F("failed to configure ACL: %v", err)
  98  		os.Exit(1)
  99  	}
 100  
 101  	// Start server
 102  	if err := srv.ListenAndServe(ctx, cancel); err != nil {
 103  		log.E.F("gRPC server error: %v", err)
 104  	}
 105  }
 106  
 107  func loadConfig() *Config {
 108  	cfg := &Config{}
 109  	if err := env.Load(cfg, nil); chk.E(err) {
 110  		log.E.F("failed to load config: %v", err)
 111  		os.Exit(1)
 112  	}
 113  
 114  	// Set default data directory if not specified
 115  	if cfg.DataDir == "" {
 116  		home, err := os.UserHomeDir()
 117  		if chk.E(err) {
 118  			log.E.F("failed to get home directory: %v", err)
 119  			os.Exit(1)
 120  		}
 121  		cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
 122  	}
 123  
 124  	// Ensure data directory exists
 125  	if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
 126  		log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
 127  		os.Exit(1)
 128  	}
 129  
 130  	return cfg
 131  }
 132  
 133  func splitList(s string) []string {
 134  	if s == "" {
 135  		return nil
 136  	}
 137  	return strings.Split(s, ",")
 138  }
 139