// Command server runs the trust relay over HTTP using the JSON transport. // // It holds no signing key: it stores and serves signed TCE objects and brokers // authentication, but never forges (INV-1) and never decides authorization // (INV-5). package main import ( "context" "flag" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "git.n1ko.dev/Niko/niko_trust/internal/server" ) func main() { configPath := flag.String("config", "config.yaml", "path to YAML config (missing file => defaults)") addr := flag.String("addr", "", "listen address (overrides config)") audience := flag.String("audience", "", "server audience bound into auth assertions (overrides config)") data := flag.String("data", "", "directory to persist objects (overrides config)") powPutBits := flag.Int("pow-put-bits", 22, "proof-of-work difficulty for storing objects, leading zero bits (0 = off)") powAuthBits := flag.Int("pow-auth-bits", 18, "proof-of-work difficulty for auth challenge issuance, leading zero bits (0 = off)") ckptInterval := flag.Duration("ckpt-interval", time.Minute, "maximum time between signed checkpoints when the object set changed") ckptEvery := flag.Int("ckpt-every", 128, "sign a checkpoint after this many new objects (0 = interval only)") bftValidators := flag.String("bft-validators", "", "comma-separated validator public keys (hex); enables BFT finality") bftURLs := flag.String("bft-urls", "", "comma-separated validator base URLs, aligned with keys") bftTimeout := flag.Duration("bft-round-timeout", 2*time.Second, "BFT round timeout") flag.Parse() cfg, err := server.LoadConfig(*configPath) if err != nil { // A missing default config is fine; only a present-but-unreadable or // malformed file is fatal. if *configPath != "config.yaml" || !os.IsNotExist(err) { slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})). Error("load config", "path", *configPath, "err", err) os.Exit(1) } } // Flags override the file. if *addr != "" { cfg.ListenAddr = *addr } if *audience != "" { cfg.Audience = *audience } if *data != "" { cfg.DataDir = *data } logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel(cfg.LogLevel)})) srv := server.New(cfg, server.WithPow(*powPutBits, *powAuthBits), server.WithCheckpoints(cfg.DataDir, server.CheckpointConfig{Interval: *ckptInterval, EveryN: *ckptEvery}), ) if *bftValidators != "" && *bftURLs != "" { srv.SetBFT(server.BFTConfig{ ValidatorKeys: splitCSV(*bftValidators), ValidatorURLs: splitCSV(*bftURLs), RoundTimeout: *bftTimeout, }) } ctx, stop := context.WithCancel(context.Background()) srv.StartCheckpoints(ctx) srv.StartBFT(ctx) defer stop() h := &http.Server{ Addr: cfg.ListenAddr, Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second, } go func() { logger.Info("trust relay listening", "addr", cfg.ListenAddr, "audience", cfg.Audience, "data_dir", cfg.DataDir) if err := h.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("listen", "err", err) os.Exit(1) } }() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig stop() logger.Info("shutting down") ctx2, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := h.Shutdown(ctx2); err != nil { logger.Error("shutdown", "err", err) } logger.Info("stopped") } func splitCSV(s string) []string { var out []string for _, part := range strings.Split(s, ",") { if part != "" { out = append(out, part) } } return out } func logLevel(s string) slog.Level { var l slog.Level if err := l.UnmarshalText([]byte(s)); err != nil { return slog.LevelInfo } return l }