package main import ( "crypto/sha256" "embed" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" ) // ---- Data model ------------------------------------------------------------- // Asset describes a downloadable artifact for a release. // Keep it minimal and verifiable. // All fields are JSON-tagged for a stable API. type Asset struct { URL string `json:"url"` SHA256 string `json:"sha256"` Size int64 `json:"size_bytes,omitempty"` SignatureURL string `json:"signature_url,omitempty"` ContentType string `json:"content_type,omitempty"` } // Release holds the metadata for a specific Branch→Channel→Arch→Bit→OS combo. // Channel is duplicated in the payload for clarity in responses. type Release struct { Version string `json:"version"` // e.g. 12.3.1 Build string `json:"build,omitempty"` // optional build id ReleasedAt time.Time `json:"released_at"` // RFC3339 NotesURL string `json:"notes_url,omitempty"` Assets []Asset `json:"assets"` Meta map[string]string `json:"meta,omitempty"` // optional free-form ChannelHint string `json:"channel,omitempty"` // echoed by server } // ProductManifest nests by Branch→Channel→Arch→Bit→OS. // Example path: Releases["12.x"]["stable"]["amd64"]["64"]["windows"] type ProductManifest struct { Product string `json:"product"` DefaultBranch string `json:"default_branch,omitempty"` DefaultChannel string `json:"default_channel,omitempty"` Releases map[string]map[string]map[string]map[string]map[string]Release `json:"releases"` } // MultiManifest groups multiple products for the same vendor. // Legacy endpoints continue to operate on DefaultProduct. type MultiManifest struct { Vendor string `json:"vendor"` UpdatedAt time.Time `json:"updated_at"` DefaultProduct string `json:"default_product,omitempty"` Products map[string]*ProductManifest `json:"products"` } // publishRequest is the payload for POST /v1/publish (and /v1/p/{product}/publish) // "product" is optional when you hit the product-scoped endpoints; required for global ones. type publishRequest struct { Product string `json:"product,omitempty"` Branch string `json:"branch"` Channel string `json:"channel"` // stable, beta, rc, nightly Arch string `json:"arch"` Bit string `json:"bit"` // "32" or "64" OS string `json:"os"` Release Release `json:"release"` } // latestResponse mirrors the request tuple alongside the release for clarity. type latestResponse struct { Product string `json:"product"` Branch string `json:"branch"` Channel string `json:"channel"` Arch string `json:"arch"` Bit string `json:"bit"` OS string `json:"os"` Release Release `json:"release"` } // ---- Store & persistence ---------------------------------------------------- type store struct { mu sync.RWMutex manifest MultiManifest path string } func newStore(path, vendor, defaultProduct string) *store { m := MultiManifest{ Vendor: vendor, UpdatedAt: time.Now().UTC(), Products: make(map[string]*ProductManifest), } if strings.TrimSpace(defaultProduct) != "" { m.DefaultProduct = defaultProduct m.Products[defaultProduct] = &ProductManifest{ Product: defaultProduct, DefaultChannel: "stable", Releases: make(map[string]map[string]map[string]map[string]map[string]Release), } } return &store{manifest: m, path: path} } // oldV1Manifest (no channels) → used for deep migration to multi. type oldV1Manifest struct { Vendor string `json:"vendor"` Product string `json:"product"` DefaultBranch string `json:"default_branch,omitempty"` UpdatedAt time.Time `json:"updated_at"` Releases map[string]map[string]map[string]map[string]Release `json:"releases"` } // oldV2Single is the previous single-product structure with channels. type oldV2Single struct { Vendor string `json:"vendor"` Product string `json:"product"` DefaultBranch string `json:"default_branch,omitempty"` DefaultChannel string `json:"default_channel,omitempty"` UpdatedAt time.Time `json:"updated_at"` Releases map[string]map[string]map[string]map[string]map[string]Release `json:"releases"` } func (s *store) loadIfExists() error { b, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return nil } return err } // Try multi manifest directly var multi MultiManifest if err := json.Unmarshal(b, &multi); err == nil && multi.Products != nil { if multi.Products == nil { multi.Products = make(map[string]*ProductManifest) } s.mu.Lock() s.manifest = multi s.mu.Unlock() return nil } // Try old V2 single-product var v2 oldV2Single if err := json.Unmarshal(b, &v2); err == nil && v2.Releases != nil { mm := MultiManifest{ Vendor: v2.Vendor, UpdatedAt: time.Now().UTC(), DefaultProduct: v2.Product, Products: make(map[string]*ProductManifest), } pm := &ProductManifest{ Product: v2.Product, DefaultBranch: v2.DefaultBranch, DefaultChannel: firstNonEmpty(v2.DefaultChannel, "stable"), Releases: v2.Releases, } mm.Products[v2.Product] = pm s.mu.Lock() s.manifest = mm s.mu.Unlock() return nil } // Try old V1 (no channels) → migrate to stable channel, then into multi var v1 oldV1Manifest if err := json.Unmarshal(b, &v1); err != nil { return fmt.Errorf("invalid manifest json: %w", err) } releases := make(map[string]map[string]map[string]map[string]map[string]Release) for br, archs := range v1.Releases { if _, ok := releases[br]; !ok { releases[br] = make(map[string]map[string]map[string]map[string]Release) } ch := releases[br] if _, ok := ch["stable"]; !ok { ch["stable"] = make(map[string]map[string]map[string]Release) } for arch, bits := range archs { if _, ok := ch["stable"][arch]; !ok { ch["stable"][arch] = make(map[string]map[string]Release) } for bit, osmap := range bits { if _, ok := ch["stable"][arch][bit]; !ok { ch["stable"][arch][bit] = make(map[string]Release) } for osname, rel := range osmap { rel.ChannelHint = "stable" ch["stable"][arch][bit][osname] = rel } } } } mm := MultiManifest{ Vendor: v1.Vendor, UpdatedAt: time.Now().UTC(), DefaultProduct: firstNonEmpty(v1.Product, s.manifest.DefaultProduct), Products: make(map[string]*ProductManifest), } if mm.DefaultProduct == "" { mm.DefaultProduct = "Product" } mm.Products[mm.DefaultProduct] = &ProductManifest{ Product: mm.DefaultProduct, DefaultBranch: v1.DefaultBranch, DefaultChannel: "stable", Releases: releases, } s.mu.Lock() s.manifest = mm s.mu.Unlock() return nil } func (s *store) persistLocked() error { // Caller must hold s.mu (write) s.manifest.UpdatedAt = time.Now().UTC() b, err := json.MarshalIndent(s.manifest, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, s.path) } func (s *store) ensureProduct(name string) *ProductManifest { p, ok := s.manifest.Products[name] if !ok { p = &ProductManifest{Product: name, DefaultChannel: "stable", Releases: make(map[string]map[string]map[string]map[string]map[string]Release)} s.manifest.Products[name] = p if s.manifest.DefaultProduct == "" { s.manifest.DefaultProduct = name } } return p } func (s *store) setLatest(pr publishRequest) error { if err := validateTuple(pr.Branch, pr.Channel, pr.Arch, pr.Bit, pr.OS); err != nil { return err } if err := validateRelease(pr.Release); err != nil { return err } product := strings.TrimSpace(pr.Product) if product == "" { product = s.manifest.DefaultProduct } if product == "" { return errors.New("product required (none configured as default)") } s.mu.Lock() defer s.mu.Unlock() pm := s.ensureProduct(product) // Create levels if missing lvl1, ok := pm.Releases[pr.Branch] if !ok { lvl1 = make(map[string]map[string]map[string]map[string]Release) pm.Releases[pr.Branch] = lvl1 } lvlCh, ok := lvl1[pr.Channel] if !ok { lvlCh = make(map[string]map[string]map[string]Release) lvl1[pr.Channel] = lvlCh } lvl2, ok := lvlCh[pr.Arch] if !ok { lvl2 = make(map[string]map[string]Release) lvlCh[pr.Arch] = lvl2 } lvl3, ok := lvl2[pr.Bit] if !ok { lvl3 = make(map[string]Release) lvl2[pr.Bit] = lvl3 } rel := pr.Release rel.ChannelHint = pr.Channel lvl3[pr.OS] = rel return s.persistLocked() } func (s *store) getLatest(product, branch, channel, arch, bit, osname string) (Release, bool) { if err := validateTuple(branch, channel, arch, bit, osname); err != nil { return Release{}, false } s.mu.RLock() defer s.mu.RUnlock() if product == "" { product = s.manifest.DefaultProduct } pm, ok := s.manifest.Products[product] if !ok { return Release{}, false } lvl1, ok := pm.Releases[branch] if !ok { return Release{}, false } lvlCh, ok := lvl1[channel] if !ok { return Release{}, false } lvl2, ok := lvlCh[arch] if !ok { return Release{}, false } lvl3, ok := lvl2[bit] if !ok { return Release{}, false } rel, ok := lvl3[osname] return rel, ok } func (s *store) branches(product string) []string { s.mu.RLock() defer s.mu.RUnlock() if product == "" { product = s.manifest.DefaultProduct } pm, ok := s.manifest.Products[product] if !ok { return nil } out := make([]string, 0, len(pm.Releases)) for k := range pm.Releases { out = append(out, k) } sort.Strings(out) return out } func (s *store) listProducts() (names []string, def string) { s.mu.RLock() defer s.mu.RUnlock() def = s.manifest.DefaultProduct for k := range s.manifest.Products { names = append(names, k) } sort.Strings(names) return } func (s *store) createOrUpdateProduct(name, defBranch, defChannel string) error { if strings.TrimSpace(name) == "" { return errors.New("product name required") } if defChannel != "" { if _, ok := allowedChannels[defChannel]; !ok { return fmt.Errorf("invalid default_channel: %s", defChannel) } } s.mu.Lock() defer s.mu.Unlock() pm, ok := s.manifest.Products[name] if !ok { pm = &ProductManifest{Product: name, DefaultChannel: firstNonEmpty(defChannel, "stable"), Releases: make(map[string]map[string]map[string]map[string]map[string]Release)} s.manifest.Products[name] = pm if s.manifest.DefaultProduct == "" { s.manifest.DefaultProduct = name } } if defBranch != "" { pm.DefaultBranch = defBranch } if defChannel != "" { pm.DefaultChannel = defChannel } return s.persistLocked() } // ---- Validation ------------------------------------------------------------- var ( allowedOS = map[string]struct{}{"windows": {}, "linux": {}, "macos": {}, "freebsd": {}} allowedArch = map[string]struct{}{"amd64": {}, "386": {}, "arm64": {}, "armv7": {}, "ppc64le": {}} allowedBit = map[string]struct{}{"64": {}, "32": {}} allowedChannels = map[string]struct{}{"stable": {}, "beta": {}, "rc": {}, "nightly": {}} ) func validateTuple(branch, channel, arch, bit, osname string) error { if strings.TrimSpace(branch) == "" { return errors.New("branch required, e.g. '12.x'") } if _, ok := allowedChannels[channel]; !ok { return fmt.Errorf("invalid channel: %s", channel) } if _, ok := allowedArch[arch]; !ok { return fmt.Errorf("invalid arch: %s", arch) } if _, ok := allowedBit[bit]; !ok { return fmt.Errorf("invalid bit: %s (use '32' or '64')", bit) } if _, ok := allowedOS[osname]; !ok { return fmt.Errorf("invalid os: %s", osname) } // Optional cross-check switch arch { case "386", "armv7": if bit != "32" { return fmt.Errorf("invalid bit %s for arch %s; expected 32", bit, arch) } case "amd64", "arm64", "ppc64le": if bit != "64" { return fmt.Errorf("invalid bit %s for arch %s; expected 64", bit, arch) } } return nil } func validateRelease(r Release) error { if strings.TrimSpace(r.Version) == "" { return errors.New("release.version required") } if r.ReleasedAt.IsZero() { return errors.New("release.released_at required (RFC3339)") } if len(r.Assets) == 0 { return errors.New("release.assets must not be empty") } for i, a := range r.Assets { if a.URL == "" { return fmt.Errorf("assets[%d].url required", i) } if a.SHA256 == "" { return fmt.Errorf("assets[%d].sha256 required", i) } } return nil } // ---- HTTP helpers ----------------------------------------------------------- func writeJSON(w http.ResponseWriter, r *http.Request, status int, v any) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") b, err := json.Marshal(v) if err != nil { http.Error(w, "json marshal error", http.StatusInternalServerError) return } etag := sha256.Sum256(b) etagStr := "\"" + hex.EncodeToString(etag[:]) + "\"" w.Header().Set("ETag", etagStr) if etagMatches(r, etagStr) { w.WriteHeader(http.StatusNotModified) return } w.WriteHeader(status) _, _ = w.Write(b) } func etagMatches(r *http.Request, etagQuoted string) bool { in := r.Header.Get("If-None-Match") if in == "" { return false } want := strings.Trim(etagQuoted, "\"") for _, part := range strings.Split(in, ",") { p := strings.TrimSpace(part) p = strings.TrimPrefix(p, "W/") p = strings.Trim(p, "\"") if p == want { return true } } return false } func parseJSON(r *http.Request, dst any) error { defer r.Body.Close() lr := io.LimitReader(r.Body, 1<<20) dec := json.NewDecoder(lr) dec.DisallowUnknownFields() return dec.Decode(dst) } func cors(w http.ResponseWriter, r *http.Request) bool { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Vary", "Origin") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type") w.WriteHeader(http.StatusNoContent) return true } return false } // ---- Handlers --------------------------------------------------------------- type server struct { st *store apiToken string // optional; if set, required for POST endpoints } func (s *server) handleHealth(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } w.WriteHeader(http.StatusNoContent) } // Global (legacy) endpoints operate on DefaultProduct to avoid breaking clients. func (s *server) handleManifest(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } product := r.URL.Query().Get("product") s.st.mu.RLock() pm := s.getProductUnsafe(product) // If no product resolved, return the full multi-manifest at _all route semantics here if pm == nil { mm := s.st.manifest s.st.mu.RUnlock() writeJSON(w, r, http.StatusOK, mm) return } m := *pm // copy (shallow) s.st.mu.RUnlock() writeJSON(w, r, http.StatusOK, m) } func (s *server) handleValues(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } product := r.URL.Query().Get("product") s.st.mu.RLock() pm := s.getProductUnsafe(product) vendor := s.st.manifest.Vendor defProduct := s.st.manifest.DefaultProduct s.st.mu.RUnlock() archs := keysOf(allowedArch) bits := keysOf(allowedBit) oss := keysOf(allowedOS) chs := keysOf(allowedChannels) resp := map[string]any{"arch": archs, "bit": bits, "os": oss, "channels": chs, "meta": map[string]string{"vendor": vendor}} if pm != nil { resp["defaults"] = map[string]string{"branch": pm.DefaultBranch, "channel": pm.DefaultChannel} resp["product"] = pm.Product } else { resp["defaults"] = map[string]string{"branch": "", "channel": "stable"} resp["product"] = defProduct } writeJSON(w, r, http.StatusOK, resp) } func (s *server) getProductUnsafe(name string) *ProductManifest { if strings.TrimSpace(name) == "" { name = s.st.manifest.DefaultProduct } if name == "" { return nil } pm := s.st.manifest.Products[name] return pm } func (s *server) handleBranches(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } product := r.URL.Query().Get("product") writeJSON(w, r, http.StatusOK, map[string]any{"branches": s.st.branches(product)}) } func (s *server) handleChannels(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } keys := keysOfMap(allowedChannels) s.st.mu.RLock() defCh := "stable" if pm := s.getProductUnsafe(""); pm != nil { defCh = pm.DefaultChannel } s.st.mu.RUnlock() writeJSON(w, r, http.StatusOK, map[string]any{"channels": keys, "default": defCh}) } func keysOfMap(m map[string]struct{}) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } func (s *server) handleLatest(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } q := r.URL.Query() product := q.Get("product") branch := q.Get("branch") channel := q.Get("channel") arch := q.Get("arch") bit := q.Get("bit") osname := q.Get("os") // Fill defaults from product s.st.mu.RLock() pm := s.getProductUnsafe(product) s.st.mu.RUnlock() if pm != nil { if branch == "" { branch = pm.DefaultBranch } if channel == "" { channel = pm.DefaultChannel } product = pm.Product } if product == "" || branch == "" || channel == "" || arch == "" || bit == "" || osname == "" { http.Error(w, "missing query params: product, branch, channel, arch, bit, os (or configure defaults)", http.StatusBadRequest) return } rel, ok := s.st.getLatest(product, branch, channel, arch, bit, osname) if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, r, http.StatusOK, latestResponse{Product: product, Branch: branch, Channel: channel, Arch: arch, Bit: bit, OS: osname, Release: rel}) } func (s *server) handleLatestPath(w http.ResponseWriter, r *http.Request) { // /v1/latest/{product}/{branch}/{channel}/{arch}/{bit}/{os} if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/latest/"), "/") if len(parts) != 6 { http.Error(w, "expected /v1/latest/{product}/{branch}/{channel}/{arch}/{bit}/{os}", http.StatusBadRequest) return } product, branch, channel, arch, bit, osname := parts[0], parts[1], parts[2], parts[3], parts[4], parts[5] rel, ok := s.st.getLatest(product, branch, channel, arch, bit, osname) if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, r, http.StatusOK, latestResponse{Product: product, Branch: branch, Channel: channel, Arch: arch, Bit: bit, OS: osname, Release: rel}) } func (s *server) handlePublish(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { cors(w, r) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if s.apiToken != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.apiToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } var pr publishRequest if err := parseJSON(r, &pr); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } // product may also be supplied via query if pr.Product == "" { pr.Product = r.URL.Query().Get("product") } if err := s.st.setLatest(pr); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) } // handleConfig allows updating vendor/product defaults. If ?product= is present, it // updates that product; otherwise, updates vendor-level defaults (DefaultProduct). func (s *server) handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { cors(w, r) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if s.apiToken != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.apiToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } product := r.URL.Query().Get("product") if product != "" { var req struct { DefaultBranch string `json:"default_branch"` DefaultChannel string `json:"default_channel"` } if err := parseJSON(r, &req); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } if req.DefaultChannel != "" { if _, ok := allowedChannels[req.DefaultChannel]; !ok { http.Error(w, "invalid default_channel", http.StatusBadRequest) return } } s.st.mu.Lock() pm := s.st.ensureProduct(product) if req.DefaultBranch != "" { pm.DefaultBranch = req.DefaultBranch } if req.DefaultChannel != "" { pm.DefaultChannel = req.DefaultChannel } if err := s.st.persistLocked(); err != nil { s.st.mu.Unlock() http.Error(w, err.Error(), http.StatusInternalServerError) return } s.st.mu.Unlock() writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) return } // Vendor/global var req struct{ Vendor, DefaultProduct string } if err := parseJSON(r, &req); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } s.st.mu.Lock() if strings.TrimSpace(req.Vendor) != "" { s.st.manifest.Vendor = req.Vendor } if strings.TrimSpace(req.DefaultProduct) != "" { if _, ok := s.st.manifest.Products[req.DefaultProduct]; ok { s.st.manifest.DefaultProduct = req.DefaultProduct } } if err := s.st.persistLocked(); err != nil { s.st.mu.Unlock() http.Error(w, err.Error(), http.StatusInternalServerError) return } s.st.mu.Unlock() writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) } // Products management func (s *server) handleProducts(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } switch r.Method { case http.MethodGet: names, def := s.st.listProducts() writeJSON(w, r, http.StatusOK, map[string]any{"products": names, "default": def}) case http.MethodPost: if s.apiToken != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.apiToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } var req struct { Product string `json:"product"` DefaultBranch string `json:"default_branch"` DefaultChannel string `json:"default_channel"` } if err := parseJSON(r, &req); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } if err := s.st.createOrUpdateProduct(req.Product, req.DefaultBranch, req.DefaultChannel); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // ---- Admin UI --------------------------------------------------------------- //go:embed admin.html var adminHTML embed.FS func (s *server) handleAdmin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") b, err := adminHTML.ReadFile("admin.html") if err != nil { http.Error(w, "admin.html not embedded; ensure //go:embed admin.html and file exists: "+err.Error(), http.StatusInternalServerError) return } sum := sha256.Sum256(b) etag := "\"" + hex.EncodeToString(sum[:]) + "\"" w.Header().Set("ETag", etag) if etagMatches(r, etag) { w.WriteHeader(http.StatusNotModified) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } // ---- Main ------------------------------------------------------------------ func main() { addr := envOr("HTTP_PUBLIC", ":8080") manifestPath := envOr("MANIFEST_PATH", "/data/manifest.json") vendor := envOr("APP_VENDOR", "YourVendor") defaultProduct := envOr("APP_PRODUCT", "YourProduct") token := envOr("API_TOKEN", "") // optional; if set, required for POST st := newStore(manifestPath, vendor, defaultProduct) if err := st.loadIfExists(); err != nil { log.Fatalf("load manifest: %v", err) } srv := &server{st: st, apiToken: token} //Default Route http.Handle("/", http.RedirectHandler("/v1/manifest", http.StatusSeeOther)) http.HandleFunc("/healthz", srv.handleHealth) http.HandleFunc("/admin", srv.handleAdmin) // Product management & global (legacy-compatible) API http.HandleFunc("/v1/products", srv.handleProducts) http.HandleFunc("/v1/manifest", srv.handleManifest) // returns default product manifest or multi when no default http.HandleFunc("/v1/values", srv.handleValues) http.HandleFunc("/v1/branches", srv.handleBranches) http.HandleFunc("/v1/channels", srv.handleChannels) http.HandleFunc("/v1/latest", srv.handleLatest) // Path variant now includes product as first segment http.HandleFunc("/v1/latest/", srv.handleLatestPath) // /v1/latest/{product}/{branch}/{channel}/{arch}/{bit}/{os} http.HandleFunc("/v1/publish", srv.handlePublish) // accepts product in JSON or ?product= http.HandleFunc("/v1/config", srv.handleConfig) // vendor or ?product= log.Printf("agent listening on %s (admin UI at /admin)", addr) log.Fatal(http.ListenAndServe(addr, nil)) } // ---- Utils ----------------------------------------------------------------- func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func firstNonEmpty(vals ...string) string { for _, v := range vals { if strings.TrimSpace(v) != "" { return v } } return "" } func keysOf(m map[string]struct{}) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out }