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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,7 @@ The CLI calls `POST /cli-auth/ci` with that Project API key; the server returns

### Guest credentials

If you run `hookdeck listen` without an existing profile, the CLI can create a **guest** sandbox (`POST /cli/guest`). The config may include `guest_url`, `guest_user_id`, and an `api_key` for that sandbox. Guest login and conversion flows are separate from `hookdeck login --cli-key` and from Project API keys.
If you run `hookdeck listen` without an existing profile, the CLI can create a **guest** sandbox (`POST /cli/guest`). The config may include `guest_url` and an `api_key` for that sandbox. `hookdeck login` reuses that existing guest key and waits for the server to upgrade the guest account into a permanent account.

### `project list` / `project use`

Expand Down
6 changes: 0 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,6 @@ func (c *Config) setProfileFieldsInViper(v *viper.Viper) {
if c.Profile.GuestURL != "" {
v.Set(c.Profile.getConfigField("guest_url"), c.Profile.GuestURL)
}
if c.Profile.GuestUserID != "" {
v.Set(c.Profile.getConfigField("guest_user_id"), c.Profile.GuestUserID)
}
}

// GetConfigFile returns the path of the currently loaded config file
Expand Down Expand Up @@ -360,8 +357,6 @@ func (c *Config) constructConfig() {

c.Profile.GuestURL = stringCoalesce(c.Profile.GuestURL, c.viper.GetString(c.Profile.getConfigField("guest_url")), c.viper.GetString("guest_url"), "")

c.Profile.GuestUserID = stringCoalesce(c.Profile.GuestUserID, c.viper.GetString(c.Profile.getConfigField("guest_user_id")), c.viper.GetString("guest_user_id"), "")

// Telemetry opt-out: check config file for telemetry_disabled = true
if c.viper.IsSet("telemetry_disabled") {
c.TelemetryDisabled = c.viper.GetBool("telemetry_disabled")
Expand Down Expand Up @@ -408,7 +403,6 @@ func zeroProfileCredentialFields(p *Profile) {
p.ProjectMode = ""
p.ProjectType = ""
p.GuestURL = ""
p.GuestUserID = ""
}

// SaveActiveProfileAfterLogin persists cfg.Profile credential fields and the active profile
Expand Down
2 changes: 0 additions & 2 deletions pkg/config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type Profile struct {
ProjectMode string
ProjectType string // display type: Gateway, Outpost, Console
GuestURL string // URL to create permanent account for guest users
GuestUserID string // Hookdeck user id for guest accounts (for signup lineage)

Config *Config
}
Expand All @@ -31,7 +30,6 @@ func (p *Profile) SaveProfile() error {
}
p.Config.viper.Set(p.getConfigField("project_type"), projectType)
p.Config.viper.Set(p.getConfigField("guest_url"), p.GuestURL)
p.Config.viper.Set(p.getConfigField("guest_user_id"), p.GuestUserID)

if err := p.removeLegacyConfigKeys(); err != nil {
return err
Expand Down
10 changes: 1 addition & 9 deletions pkg/config/profile_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ func (p *Profile) ApplyValidateAPIKeyResponse(resp *hookdeck.ValidateAPIKeyRespo
p.ProjectType = ModeToProjectType(resp.ProjectMode)
if clearGuestURL {
p.GuestURL = ""
p.GuestUserID = ""
}
}

// ApplyPollAPIKeyResponse applies credentials from a completed CLI auth poll (browser or interactive login).
// guestURL is the guest upgrade URL when applicable; use "" for a normal account login.
// guestUserID is the Hookdeck user id when the session is a guest account.
func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, guestURL string, guestUserID string) {
func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, guestURL string) {
if resp == nil {
return
}
Expand All @@ -30,11 +28,6 @@ func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, gue
p.ProjectMode = resp.ProjectMode
p.ProjectType = ModeToProjectType(resp.ProjectMode)
p.GuestURL = guestURL
if guestUserID != "" {
p.GuestUserID = guestUserID
} else if guestURL == "" {
p.GuestUserID = ""
}
}

// ApplyCIClient applies credentials from hookdeck login --ci.
Expand All @@ -44,5 +37,4 @@ func (p *Profile) ApplyCIClient(ci hookdeck.CIClient) {
p.ProjectMode = ci.ProjectMode
p.ProjectType = ModeToProjectType(ci.ProjectMode)
p.GuestURL = ""
p.GuestUserID = ""
}
7 changes: 3 additions & 4 deletions pkg/config/profile_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestProfile_ApplyValidateAPIKeyResponse(t *testing.T) {
func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
t.Run("nil response is no-op", func(t *testing.T) {
p := &Profile{APIKey: "k", ProjectId: "p"}
p.ApplyPollAPIKeyResponse(nil, "", "")
p.ApplyPollAPIKeyResponse(nil, "")
require.Equal(t, "k", p.APIKey)
require.Equal(t, "p", p.ProjectId)
})
Expand All @@ -56,12 +56,11 @@ func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
APIKey: "key_from_poll",
ProjectID: "team_p",
ProjectMode: "inbound",
}, "https://guest", "guest_user_1")
}, "https://guest")
require.Equal(t, "key_from_poll", p.APIKey)
require.Equal(t, "team_p", p.ProjectId)
require.Equal(t, ProjectTypeGateway, p.ProjectType)
require.Equal(t, "https://guest", p.GuestURL)
require.Equal(t, "guest_user_1", p.GuestUserID)
})

t.Run("clears guest URL when empty string passed", func(t *testing.T) {
Expand All @@ -70,7 +69,7 @@ func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
APIKey: "k123456789012",
ProjectID: "t",
ProjectMode: "inbound",
}, "", "")
}, "")
require.Empty(t, p.GuestURL)
})
}
Expand Down
5 changes: 2 additions & 3 deletions pkg/gateway/mcp/tool_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"

"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
"github.com/hookdeck/hookdeck-cli/pkg/login"
"github.com/hookdeck/hookdeck-cli/pkg/project"
"github.com/hookdeck/hookdeck-cli/pkg/validators"
)
Expand Down Expand Up @@ -123,7 +122,7 @@ func handleLogin(srv *Server) mcpsdk.ToolHandler {
}

authClient := &hookdeck.Client{BaseURL: parsedBaseURL, TelemetryDisabled: cfg.TelemetryDisabled}
session, err := authClient.StartLogin(login.BuildStartLoginInput(cfg, ""))
session, err := authClient.StartLogin(cfg.DeviceName)
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to start login: %s", err)), nil
}
Expand Down Expand Up @@ -173,7 +172,7 @@ func handleLogin(srv *Server) mcpsdk.ToolHandler {
}

// Persist credentials so future MCP sessions start authenticated.
cfg.Profile.ApplyPollAPIKeyResponse(response, "", "")
cfg.Profile.ApplyPollAPIKeyResponse(response, "")

cfg.SaveActiveProfileAfterLogin()

Expand Down
35 changes: 17 additions & 18 deletions pkg/hookdeck/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ValidateAPIKeyResponse struct {
UserID string `json:"user_id"`
UserName string `json:"user_name"`
UserEmail string `json:"user_email"`
UserIsGuest bool `json:"user_is_guest"`
OrganizationName string `json:"organization_name"`
OrganizationID string `json:"organization_id"`
ProjectID string `json:"team_id"`
Expand Down Expand Up @@ -49,13 +50,6 @@ type UpdateClientInput struct {
DeviceName string `json:"device_name"`
}

// StartLoginInput configures POST /cli-auth for browser login.
type StartLoginInput struct {
DeviceName string `json:"device_name"`
GuestUserID string `json:"guest_user_id,omitempty"`
GuestAPIKey string `json:"guest_api_key,omitempty"`
}

// LoginSession represents an in-progress login flow
type LoginSession struct {
BrowserURL string
Expand All @@ -64,15 +58,20 @@ type LoginSession struct {

// GuestSession represents an in-progress guest login flow
type GuestSession struct {
BrowserURL string
GuestURL string
GuestUserID string
pollURL string
BrowserURL string
GuestURL string
pollURL string
}

// StartLogin initiates the login flow and returns a session to wait for completion
func (c *Client) StartLogin(input StartLoginInput) (*LoginSession, error) {
jsonData, err := json.Marshal(input)
func (c *Client) StartLogin(deviceName string) (*LoginSession, error) {
data := struct {
DeviceName string `json:"device_name"`
}{
DeviceName: deviceName,
}

jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -106,17 +105,17 @@ func (c *Client) StartLogin(input StartLoginInput) (*LoginSession, error) {
// StartGuestLogin initiates a guest login flow and returns a session to wait for completion
func (c *Client) StartGuestLogin(deviceName string) (*GuestSession, error) {
guest, err := c.CreateGuestUser(CreateGuestUserInput{
DeviceName: deviceName,
DeviceName: deviceName,
LinkContext: "signup",
})
if err != nil {
return nil, err
}

return &GuestSession{
BrowserURL: guest.BrowserURL,
GuestURL: guest.Url,
GuestUserID: guest.Id,
pollURL: guest.PollURL,
BrowserURL: guest.BrowserURL,
GuestURL: guest.Url,
pollURL: guest.PollURL,
}, nil
}

Expand Down
9 changes: 7 additions & 2 deletions pkg/hookdeck/guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type GuestSigninLinkResponse struct {
}

type CreateGuestUserInput struct {
DeviceName string `json:"device_name"`
DeviceName string `json:"device_name,omitempty"`
LinkContext string `json:"link_context,omitempty"`
}

func (c *Client) CreateGuestUser(input CreateGuestUserInput) (GuestUser, error) {
Expand All @@ -44,7 +45,11 @@ func (c *Client) CreateGuestUser(input CreateGuestUserInput) (GuestUser, error)
}

func (c *Client) RefreshGuestSigninLink() (GuestSigninLinkResponse, error) {
res, err := c.Post(context.Background(), APIPathPrefix+"/cli/guest/signin-link", nil, nil)
input_bytes, err := json.Marshal(CreateGuestUserInput{LinkContext: "signup"})
if err != nil {
return GuestSigninLinkResponse{}, err
}
res, err := c.Post(context.Background(), APIPathPrefix+"/cli/guest", input_bytes, nil)
if err != nil {
return GuestSigninLinkResponse{}, err
}
Expand Down
63 changes: 31 additions & 32 deletions pkg/hookdeck/request_log_redact_test.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
package hookdeck

import (
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func TestRedactHeadersForLog_redactsAuthorization(t *testing.T) {
headers := http.Header{}
headers.Set("Authorization", "Basic c2tfdGVzdDo=")
headers.Set("Content-Type", "application/json")

redacted := redactHeadersForLog(headers)
require.Equal(t, "[redacted]", redacted.Get("Authorization"))
require.Equal(t, "application/json", redacted.Get("Content-Type"))
require.Equal(t, "Basic c2tfdGVzdDo=", headers.Get("Authorization"))
}

func TestRedactRequestBodyForLog_redactsGuestAPIKey(t *testing.T) {
body := `{"device_name":"laptop","guest_user_id":"usr_1","guest_api_key":"hk_secret"}`
redacted := redactRequestBodyForLog(body)
require.Contains(t, redacted, `"guest_api_key":"[redacted]"`)
require.Contains(t, redacted, `"guest_user_id":"usr_1"`)
require.NotContains(t, redacted, "hk_secret")
}

func TestRedactRequestBodyForLog_leavesNonJSONUnchanged(t *testing.T) {
body := "guest_api_key=hk_secret"
require.Equal(t, body, redactRequestBodyForLog(body))
}
package hookdeck

import (
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func TestRedactHeadersForLog_redactsAuthorization(t *testing.T) {
headers := http.Header{}
headers.Set("Authorization", "Basic c2tfdGVzdDo=")
headers.Set("Content-Type", "application/json")

redacted := redactHeadersForLog(headers)
require.Equal(t, "[redacted]", redacted.Get("Authorization"))
require.Equal(t, "application/json", redacted.Get("Content-Type"))
require.Equal(t, "Basic c2tfdGVzdDo=", headers.Get("Authorization"))
}

func TestRedactRequestBodyForLog_redactsGuestAPIKey(t *testing.T) {
body := `{"device_name":"laptop","guest_api_key":"hk_secret"}`
redacted := redactRequestBodyForLog(body)
require.Contains(t, redacted, `"guest_api_key":"[redacted]"`)
require.NotContains(t, redacted, "hk_secret")
}

func TestRedactRequestBodyForLog_leavesNonJSONUnchanged(t *testing.T) {
body := "guest_api_key=hk_secret"
require.Equal(t, body, redactRequestBodyForLog(body))
}
Loading
Loading