package httpapi import ( "crypto/ed25519" "crypto/rand" "encoding/hex" "errors" "net/http" "strings" "evobgp/internal/jobs" "evobgp/internal/observability" "evobgp/internal/store" ) // Server implements EvoBGP control-plane HTTP API (subset focused on jobs, deploy, node bundle). type Server struct { store *store.Memory jobs *jobs.Registry bundlePriv ed25519.PrivateKey apiKeys []apiKeyRecord insecureDev bool corsOrigins []string mux *http.ServeMux } // Options configures the API server. type Options struct { // APIKeys is comma-separated "token|tenantUUID|role" (role: viewer, editor, operator, node). APIKeys string // InsecureDev with SeedDemo allows Bearer "dev" as operator for the demo tenant (local only). InsecureDev bool SeedDemo bool // BundleSeedHex is 64 hex chars (32 bytes) for deterministic Ed25519 bundle signing; if empty, random. BundleSeedHex string // CORSAllowedOrigins is comma-separated list of allowed browser Origins (e.g. http://localhost:4173). CORSAllowedOrigins string } // New constructs Server and wiring for async jobs. func New(opts Options) (*Server, error) { mem := store.NewMemory() if opts.SeedDemo { mem.SeedDemo() } wk := &jobs.Worker{Store: mem} reg := jobs.NewRegistry(wk.Process) var priv ed25519.PrivateKey if strings.TrimSpace(opts.BundleSeedHex) != "" { seed, err := hex.DecodeString(strings.TrimSpace(opts.BundleSeedHex)) if err != nil { return nil, err } if len(seed) != ed25519.SeedSize { return nil, errors.New("httpapi: BundleSeedHex must decode to 32 bytes") } priv = ed25519.NewKeyFromSeed(seed) } else { _, priv, _ = ed25519.GenerateKey(rand.Reader) } s := &Server{ store: mem, jobs: reg, bundlePriv: priv, apiKeys: parseAPIKeysSpec(opts.APIKeys), insecureDev: opts.InsecureDev && opts.SeedDemo, corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins), } observability.RegisterStoreMetrics(mem) s.mux = http.NewServeMux() s.registerRoutes() return s, nil } // Store exposes the in-memory store (for operators / tests). func (s *Server) Store() *store.Memory { return s.store }