Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 0 additions & 54 deletions cmd/asobi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key>")
}
return cfg.URL, cfg.APIKey
}

// --- Health ---

func cmdHealth() {
Expand Down Expand Up @@ -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() {
Expand Down
61 changes: 0 additions & 61 deletions internal/auth/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
Expand Down Expand Up @@ -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")
}
}
91 changes: 0 additions & 91 deletions internal/auth/saas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
61 changes: 0 additions & 61 deletions internal/client/client.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"

Expand All @@ -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 {
Expand Down