From a4f1c692dc3a54ae89a7fd9f145c87a98a873da9 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 3 Jul 2026 15:29:26 +0200 Subject: [PATCH] refactor: remove dead direct-to-engine deploy + key-mint paths asobi deploy is a pull model: the CLI uploads the bundle to saas over its Bearer access token (auth.DeployBundle), saas stores it, and the engine pulls its own bundle with x-engine-key. The CLI never holds an engine credential. These paths were orphaned (no callers): - main.go: resolveDeployCredentials, cmdDeployEphemeral - auth/saas.go: MintKey, EphemeralDeploy (+ their response types) - client/client.go: Client.Deploy, DeployRequest, DeployResponse (kept Script, used by internal/deploy) - auth/device_test.go: the two MintKey tests Removes the ephemeral cli_deploy key-mint client surface. build/vet/test green. First step of the credential-model cleanup (server-side mint/ validate routes follow). --- cmd/asobi/main.go | 54 --------------------- internal/auth/device_test.go | 61 ------------------------ internal/auth/saas.go | 91 ------------------------------------ internal/client/client.go | 61 ------------------------ 4 files changed, 267 deletions(-) diff --git a/cmd/asobi/main.go b/cmd/asobi/main.go index c6a5c7f..9352605 100644 --- a/cmd/asobi/main.go +++ b/cmd/asobi/main.go @@ -238,27 +238,6 @@ func cmdDeploy() { fmt.Printf("\nDeployed to %s (game: %s)! generation=%d sha256=%s\n", envName, game, int(gen), sha[:12]+"...") } -func resolveDeployCredentials() (engineURL, apiKey string) { - creds, _ := auth.LoadCredentials() - if creds != nil && creds.AccessToken != "" { - fmt.Println("Minting ephemeral deploy key...") - key, err := auth.MintKey(creds) - if err != nil { - fatal("mint deploy key: %v\nTry: asobi login", err) - } - return creds.EngineURL, key - } - - cfg, err := config.Load() - if err != nil { - fatal("load config: %v", err) - } - if cfg.APIKey == "" { - fatal("not logged in and no API key configured.\n\nRun: asobi login\n or: asobi config set api_key ") - } - return cfg.URL, cfg.APIKey -} - // --- Health --- func cmdHealth() { @@ -436,39 +415,6 @@ func fatal(format string, args ...any) { os.Exit(1) } -// --- Ephemeral deploy --- - -func cmdDeployEphemeral(name string, jsonOut bool) { - creds, err := auth.LoadCredentials() - if err != nil || creds == nil || creds.AccessToken == "" { - fatal("not logged in. Run: asobi login") - } - - if !jsonOut { - fmt.Println("Creating ephemeral environment (1h TTL)...") - } - resp, err := auth.EphemeralDeploy(creds, name) - if err != nil { - fatal("ephemeral-deploy: %v", err) - } - - if jsonOut { - out, _ := json.Marshal(map[string]any{ - "env_id": resp.EnvID, - "api_key": resp.RawKey, - "expires_in": resp.ExpiresIn, - }) - fmt.Println(string(out)) - return - } - - fmt.Printf("\n🦝 Ephemeral environment created!\n") - fmt.Printf(" env_id: %s\n", resp.EnvID) - fmt.Printf(" api_key: %s\n", resp.RawKey) - fmt.Printf(" expires_in: %ds (~%dm)\n", resp.ExpiresIn, resp.ExpiresIn/60) - fmt.Printf("\nTo destroy explicitly: asobi destroy %s\n", resp.EnvID) -} - // --- Destroy --- func cmdDestroy() { diff --git a/internal/auth/device_test.go b/internal/auth/device_test.go index 11090c5..f923c35 100644 --- a/internal/auth/device_test.go +++ b/internal/auth/device_test.go @@ -3,7 +3,6 @@ package auth import ( "encoding/base64" "encoding/json" - "fmt" "net/http" "net/http/httptest" "sync" @@ -136,63 +135,3 @@ func TestLoginDenied(t *testing.T) { t.Errorf("error = %q, want %q", err.Error(), want) } } - -func TestMintKeyHappyPath(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("/internal/cli/mint-key", func(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - if auth != "Bearer test-access-token" { - w.WriteHeader(401) - fmt.Fprintf(w, `{"error":"invalid_token"}`) - return - } - json.NewEncoder(w).Encode(map[string]any{ - "raw_key": "ak_test1234_deadbeef", - "expires_in": 3600, - }) - }) - - server := httptest.NewServer(mux) - defer server.Close() - - creds := &Credentials{ - AccessToken: "test-access-token", - SaasURL: server.URL, - DeviceFingerprint: "test-host", - } - - key, err := MintKey(creds) - if err != nil { - t.Fatalf("MintKey: %v", err) - } - if key != "ak_test1234_deadbeef" { - t.Errorf("key = %q, want ak_test1234_deadbeef", key) - } -} - -func TestMintKeyBadToken(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("/internal/cli/mint-key", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(401) - fmt.Fprintf(w, `{"error":"invalid_token"}`) - }) - mux.HandleFunc("/internal/cli/refresh", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(401) - fmt.Fprintf(w, `{"error":"refresh_expired"}`) - }) - - server := httptest.NewServer(mux) - defer server.Close() - - creds := &Credentials{ - AccessToken: "bad-token", - RefreshToken: "also-bad", - SaasURL: server.URL, - DeviceFingerprint: "test-host", - } - - _, err := MintKey(creds) - if err == nil { - t.Fatal("expected error for bad token") - } -} diff --git a/internal/auth/saas.go b/internal/auth/saas.go index b9fd167..52d2fad 100644 --- a/internal/auth/saas.go +++ b/internal/auth/saas.go @@ -9,20 +9,6 @@ import ( "time" ) -type mintKeyResponse struct { - RawKey string `json:"raw_key"` - ExpiresIn int `json:"expires_in"` - Error string `json:"error,omitempty"` -} - -// EphemeralDeploy creates a fresh ephemeral environment + API key with 1h TTL. -type EphemeralDeployResponse struct { - EnvID string `json:"env_id"` - RawKey string `json:"raw_key"` - ExpiresIn int `json:"expires_in"` - Error string `json:"error,omitempty"` -} - // Environment is a single env returned by ListEnvs. type Environment struct { ID string `json:"id"` @@ -51,46 +37,6 @@ type refreshResponse struct { var httpClient = &http.Client{Timeout: 10 * time.Second} -// MintKey calls the saas to create a short-lived engine API key. -// Uses the CLI's access token as a bearer credential. -func MintKey(creds *Credentials) (string, error) { - url := creds.SaasURL + "/internal/cli/mint-key" - req, err := http.NewRequest("POST", url, bytes.NewReader([]byte("{}"))) - if err != nil { - return "", fmt.Errorf("create request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+creds.AccessToken) - - resp, err := httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("POST /internal/cli/mint-key: %w", err) - } - defer resp.Body.Close() - data, _ := io.ReadAll(resp.Body) - - if resp.StatusCode == 401 { - refreshed, refreshErr := RefreshAccessToken(creds) - if refreshErr != nil { - return "", fmt.Errorf("access token expired and refresh failed: %w (original: %s)", refreshErr, data) - } - creds.AccessToken = refreshed - if saveErr := SaveCredentials(creds); saveErr != nil { - return "", fmt.Errorf("save refreshed credentials: %w", saveErr) - } - return MintKey(creds) - } - - var result mintKeyResponse - if err := json.Unmarshal(data, &result); err != nil { - return "", fmt.Errorf("parse mint-key response: %w", err) - } - if resp.StatusCode != 200 { - return "", fmt.Errorf("mint-key failed (%d): %s", resp.StatusCode, result.Error) - } - return result.RawKey, nil -} - // RefreshAccessToken exchanges a refresh token for a new access token. // The device secret issued at login must match the hash stored server-side. func RefreshAccessToken(creds *Credentials) (string, error) { @@ -125,43 +71,6 @@ func mustNewRequest(method, url string, body []byte) *http.Request { return req } -// EphemeralDeploy creates a fresh ephemeral env + API key with 1h TTL. -func EphemeralDeploy(creds *Credentials, name string) (*EphemeralDeployResponse, error) { - body, _ := json.Marshal(map[string]string{"name": name}) - req, err := http.NewRequest("POST", creds.SaasURL+"/internal/cli/ephemeral-deploy", bytes.NewReader(body)) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+creds.AccessToken) - - resp, err := httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("POST /internal/cli/ephemeral-deploy: %w", err) - } - defer resp.Body.Close() - data, _ := io.ReadAll(resp.Body) - - if resp.StatusCode == 401 { - refreshed, refreshErr := RefreshAccessToken(creds) - if refreshErr != nil { - return nil, fmt.Errorf("access token expired and refresh failed: %w", refreshErr) - } - creds.AccessToken = refreshed - _ = SaveCredentials(creds) - return EphemeralDeploy(creds, name) - } - - var result EphemeralDeployResponse - if err := json.Unmarshal(data, &result); err != nil { - return nil, fmt.Errorf("parse response: %w", err) - } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("ephemeral-deploy failed (%d): %s", resp.StatusCode, result.Error) - } - return &result, nil -} - // Destroy deletes an environment by ID. Idempotent. func Destroy(creds *Credentials, envID string) error { url := creds.SaasURL + "/internal/cli/environments/" + envID diff --git a/internal/client/client.go b/internal/client/client.go index 7b7006b..c36a3cf 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,10 +1,7 @@ package client import ( - "bytes" "encoding/json" - "fmt" - "io" "net/http" "time" @@ -27,69 +24,11 @@ func (c *Client) Health() (map[string]any, error) { return c.get("/health") } -type DeployRequest struct { - Scripts []Script `json:"scripts"` -} - type Script struct { Path string `json:"path"` Content string `json:"content"` } -type DeployResponse struct { - Deployed int `json:"deployed"` - Scripts []string `json:"scripts"` - Error string `json:"error,omitempty"` - Message string `json:"message,omitempty"` -} - -func (c *Client) Deploy(scripts []Script) (*DeployResponse, error) { - body, err := json.Marshal(DeployRequest{Scripts: scripts}) - if err != nil { - return nil, fmt.Errorf("marshal: %w", err) - } - - req, err := http.NewRequest("POST", c.cfg.URL+"/internal/deploy", bytes.NewReader(body)) - if err != nil { - return nil, fmt.Errorf("request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("x-api-key", c.cfg.APIKey) - - resp, err := c.http.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read response: %w", err) - } - - if len(data) == 0 { - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return &DeployResponse{Deployed: len(scripts)}, nil - } - return nil, fmt.Errorf("deploy failed (%d): empty response", resp.StatusCode) - } - - var result DeployResponse - if err := json.Unmarshal(data, &result); err != nil { - return nil, fmt.Errorf("parse response (%d): %s", resp.StatusCode, string(data)) - } - - if resp.StatusCode != 200 { - msg := result.Message - if msg == "" { - msg = result.Error - } - return nil, fmt.Errorf("deploy failed (%d): %s", resp.StatusCode, msg) - } - - return &result, nil -} - func (c *Client) get(path string) (map[string]any, error) { req, err := http.NewRequest("GET", c.cfg.URL+path, nil) if err != nil {