Skip to content
Open
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
58 changes: 49 additions & 9 deletions packages/cli/internal/cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
)

const apiProjectsPath = "/api/projects/"
Expand Down Expand Up @@ -134,9 +135,9 @@ func (c APIClient) ListSessions(ctx context.Context, projectID string) (SessionL
return response, err
}

func (c APIClient) GetSessionMessages(ctx context.Context, projectID string, sessionID string) (MessageListResponse, error) {
var response MessageListResponse
err := c.request(ctx, http.MethodGet, projectAPIPath(projectID, "sessions", sessionID, "messages"), nil, &response)
func (c APIClient) GetSessionDetail(ctx context.Context, projectID string, sessionID string) (SessionDetailResponse, error) {
var response SessionDetailResponse
err := c.request(ctx, http.MethodGet, projectAPIPath(projectID, "sessions", sessionID), nil, &response)
return response, err
}

Expand All @@ -146,9 +147,13 @@ func (c APIClient) ListIdeas(ctx context.Context, projectID string) (IdeaListRes
return response, err
}

func (c APIClient) ListLibraryFiles(ctx context.Context, projectID string) (LibraryListResponse, error) {
func (c APIClient) ListLibraryFiles(ctx context.Context, projectID string, recursive bool) (LibraryListResponse, error) {
var response LibraryListResponse
err := c.request(ctx, http.MethodGet, projectAPIPath(projectID, "library"), nil, &response)
path := projectAPIPath(projectID, "library")
if recursive {
path += "?" + url.Values{"recursive": {"true"}}.Encode()
}
err := c.request(ctx, http.MethodGet, path, nil, &response)
return response, err
}

Expand Down Expand Up @@ -183,9 +188,9 @@ func (c APIClient) ListActivity(ctx context.Context, projectID string) (Activity
}

func (c APIClient) ListNodes(ctx context.Context) (NodeListResponse, error) {
var response NodeListResponse
err := c.request(ctx, http.MethodGet, "/api/nodes", nil, &response)
return response, err
var nodes []Node
err := c.request(ctx, http.MethodGet, "/api/nodes", nil, &nodes)
return NodeListResponse{Nodes: nodes}, err
}

func projectAPIPath(projectID string, segments ...string) string {
Expand Down Expand Up @@ -238,11 +243,46 @@ func doJSON(ctx context.Context, httpClient HTTPDoer, method string, endpoint st
return nil
}
if err := json.Unmarshal(content, out); err != nil {
return APIError{Status: response.StatusCode, Code: "INVALID_JSON", Message: "SAM API returned invalid JSON"}
return APIError{
Status: response.StatusCode,
Code: "INVALID_JSON",
Message: fmt.Sprintf(
"SAM API returned invalid JSON for %s %s (status %d): %v",
method,
safeEndpointPath(endpoint),
response.StatusCode,
err,
),
}
}
return nil
}

func safeEndpointPath(endpoint string) string {
parsed, err := url.Parse(endpoint)
if err != nil {
return "<invalid-url>"
}
if parsed.RawQuery == "" {
return parsed.EscapedPath()
}
query := parsed.Query()
for key := range query {
if isSensitiveQueryKey(key) {
query.Set(key, "REDACTED")
}
}
return parsed.EscapedPath() + "?" + query.Encode()
}

func isSensitiveQueryKey(key string) bool {
normalized := strings.ToLower(key)
return strings.Contains(normalized, "token") ||
strings.Contains(normalized, "cookie") ||
strings.Contains(normalized, "secret") ||
strings.Contains(normalized, "session")
}

func parseAPIError(status int, content []byte) error {
var body struct {
Error string `json:"error"`
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/internal/cli/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,27 @@ func TestAPIInvalidJSONErrorIsActionableAndRedacted(t *testing.T) {
t.Fatalf("error leaked cookie: %q", err.Error())
}
}

func TestAPIInvalidJSONErrorRedactsSensitiveQueryValues(t *testing.T) {
err := doJSON(
context.Background(),
roundTripFunc(func(*http.Request) (*http.Response, error) {
return jsonResponse(`not-json`, http.StatusOK), nil
}),
http.MethodGet,
"https://api.example.com/api/demo?token=secret-token&cursor=abc",
"cookie=value",
nil,
&map[string]any{},
)
if err == nil {
t.Fatal("expected invalid JSON error")
}
message := err.Error()
if strings.Contains(message, "secret-token") || strings.Contains(message, "cookie=value") {
t.Fatalf("error leaked secret: %q", message)
}
if !strings.Contains(message, "token=REDACTED") || !strings.Contains(message, "cursor=abc") {
t.Fatalf("error missing safe query context: %q", message)
}
}
Loading
Loading