254 lines
7.2 KiB
Go
254 lines
7.2 KiB
Go
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",
|
|
})
|
|
}
|
|
}
|