package handlers import ( "encoding/json" "io" "net/http" "strconv" "forge-mesh/internal/auth" "forge-mesh/internal/court" "forge-mesh/internal/erasure" "forge-mesh/internal/fleet" ) // IntelligenceDeps bundles triple-onion fleet intelligence handlers. type IntelligenceDeps struct { Store *fleet.Store Subnet *fleet.SubnetMapper Earn *fleet.EarnGate } // RunLOTL handles POST /api/v1/fleet/{id}/lotl/run — execute tier chain with recon. func RunLOTL(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { hostID := r.PathValue("id") if hostID == "" { http.Error(w, "host id required", http.StatusBadRequest) return } strategy := fleet.AdaptiveStrategy{Store: deps.Store} order, err := strategy.OrderForHost(r.Context(), hostID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } results, err := fleet.RunTierChain(r.Context(), deps.Store, hostID, order) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]interface{}{ "host_id": hostID, "order": order, "results": results, }) } } // ListLOTL handles GET /api/v1/fleet/{id}/lotl. func ListLOTL(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { hostID := r.PathValue("id") attempts, err := deps.Store.ListLOTL(r.Context(), hostID, 100) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]interface{}{ "host_id": hostID, "attempts": attempts, }) } } // SpreadGate handles GET /api/v1/fleet/{id}/spread-gate (earn-before-burn). func SpreadGate(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { hostID := r.PathValue("id") dec, err := deps.Earn.CanSpreadToSiblings(r.Context(), hostID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, dec) } } // SubnetList handles GET /api/v1/fleet/subnets. func SubnetList(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cidrs, err := deps.Subnet.ListCIDRs(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]interface{}{"cidrs": cidrs}) } } // SubnetAdd handles POST /api/v1/fleet/subnets. func SubnetAdd(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { CIDR string `json:"cidr"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.CIDR == "" { http.Error(w, "cidr required", http.StatusBadRequest) return } c, err := deps.Subnet.AddCIDR(r.Context(), req.CIDR) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } auth.JSON(w, http.StatusCreated, c) } } // SubnetSweep handles POST /api/v1/fleet/subnets/sweep. func SubnetSweep(deps IntelligenceDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { results, err := deps.Subnet.SweepAll(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]interface{}{"results": results}) } } // CourtDeps bundles court handler dependencies. type CourtDeps struct { Court *court.Court Seer *court.SeerHub } // CourtOpen handles POST /api/v1/court/sessions. func CourtOpen(deps CourtDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { HostID string `json:"host_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.HostID == "" { http.Error(w, "host_id required", http.StatusBadRequest) return } s, err := deps.Court.OpenSession(r.Context(), req.HostID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusCreated, s) } } // CourtDeliberate handles POST /api/v1/court/sessions/{id}/deliberate. func CourtDeliberate(deps CourtDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") s, err := deps.Court.Deliberate(r.Context(), id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, s) } } // CourtVerdict handles POST /api/v1/court/sessions/{id}/verdict (L4). func CourtVerdict(deps CourtDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req struct { Verdict string `json:"verdict"` Clearance int `json:"clearance"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Verdict == "" { http.Error(w, "verdict required", http.StatusBadRequest) return } if req.Clearance == 0 { req.Clearance = 4 } if err := deps.Court.DispatchVerdict(r.Context(), id, req.Verdict, req.Clearance); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } auth.JSON(w, http.StatusOK, map[string]string{"ok": "true", "verdict": req.Verdict}) } } // Timeline handles GET /api/v1/fleet/{id}/timeline (LOTL + court). func Timeline(deps CourtDeps) http.HandlerFunc { return court.TimelineHandler(deps.Court) } // ErasureDeps bundles public erasure routes. type ErasureDeps struct { Service *erasure.Service } // ErasureEncode handles POST /api/v1/public/erasure/encode (dev/admin via basic elsewhere). func ErasureEncode(deps ErasureDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "bundle" } data, err := io.ReadAll(io.LimitReader(r.Body, 16<<20)) if err != nil { http.Error(w, "read error", http.StatusBadRequest) return } b, err := deps.Service.Encode(r.Context(), name, data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } auth.JSON(w, http.StatusCreated, b) } } // ErasureShard handles GET /api/v1/public/erasure/{bundle_id}/shard/{index}. func ErasureShard(deps ErasureDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { bundleID := r.PathValue("bundle_id") idx, err := strconv.Atoi(r.PathValue("index")) if err != nil { http.Error(w, "invalid index", http.StatusBadRequest) return } sh, err := deps.Service.GetShard(r.Context(), bundleID, idx) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("X-Shard-Index", strconv.Itoa(sh.ShardIndex)) _, _ = w.Write(sh.Data) } } // ErasureBundleMeta handles GET /api/v1/public/erasure/{bundle_id}. func ErasureBundleMeta(deps ErasureDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { bundleID := r.PathValue("bundle_id") b, err := deps.Service.GetBundle(r.Context(), bundleID) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } indices, _ := deps.Service.ListShards(r.Context(), bundleID) auth.JSON(w, http.StatusOK, map[string]interface{}{ "bundle": b, "shards": indices, "public": true, "scheme": "RS_4_2", }) } }