Skip to content

Commit acad4fe

Browse files
committed
Simplify CLI guest login upgrade
1 parent 0516e3d commit acad4fe

18 files changed

Lines changed: 641 additions & 661 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1626,7 +1626,7 @@ The CLI calls `POST /cli-auth/ci` with that Project API key; the server returns
16261626

16271627
### Guest credentials
16281628

1629-
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.
1629+
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.
16301630

16311631
### `project list` / `project use`
16321632

pkg/config/config.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,6 @@ func (c *Config) setProfileFieldsInViper(v *viper.Viper) {
251251
if c.Profile.GuestURL != "" {
252252
v.Set(c.Profile.getConfigField("guest_url"), c.Profile.GuestURL)
253253
}
254-
if c.Profile.GuestUserID != "" {
255-
v.Set(c.Profile.getConfigField("guest_user_id"), c.Profile.GuestUserID)
256-
}
257254
}
258255

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

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

363-
c.Profile.GuestUserID = stringCoalesce(c.Profile.GuestUserID, c.viper.GetString(c.Profile.getConfigField("guest_user_id")), c.viper.GetString("guest_user_id"), "")
364-
365360
// Telemetry opt-out: check config file for telemetry_disabled = true
366361
if c.viper.IsSet("telemetry_disabled") {
367362
c.TelemetryDisabled = c.viper.GetBool("telemetry_disabled")
@@ -408,7 +403,6 @@ func zeroProfileCredentialFields(p *Profile) {
408403
p.ProjectMode = ""
409404
p.ProjectType = ""
410405
p.GuestURL = ""
411-
p.GuestUserID = ""
412406
}
413407

414408
// SaveActiveProfileAfterLogin persists cfg.Profile credential fields and the active profile

pkg/config/profile.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ type Profile struct {
1111
ProjectMode string
1212
ProjectType string // display type: Gateway, Outpost, Console
1313
GuestURL string // URL to create permanent account for guest users
14-
GuestUserID string // Hookdeck user id for guest accounts (for signup lineage)
1514

1615
Config *Config
1716
}
@@ -31,7 +30,6 @@ func (p *Profile) SaveProfile() error {
3130
}
3231
p.Config.viper.Set(p.getConfigField("project_type"), projectType)
3332
p.Config.viper.Set(p.getConfigField("guest_url"), p.GuestURL)
34-
p.Config.viper.Set(p.getConfigField("guest_user_id"), p.GuestUserID)
3533
return p.Config.writeConfig()
3634
}
3735

pkg/config/profile_credentials.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ func (p *Profile) ApplyValidateAPIKeyResponse(resp *hookdeck.ValidateAPIKeyRespo
1414
p.ProjectType = ModeToProjectType(resp.ProjectMode)
1515
if clearGuestURL {
1616
p.GuestURL = ""
17-
p.GuestUserID = ""
1817
}
1918
}
2019

2120
// ApplyPollAPIKeyResponse applies credentials from a completed CLI auth poll (browser or interactive login).
2221
// guestURL is the guest upgrade URL when applicable; use "" for a normal account login.
23-
// guestUserID is the Hookdeck user id when the session is a guest account.
24-
func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, guestURL string, guestUserID string) {
22+
func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, guestURL string) {
2523
if resp == nil {
2624
return
2725
}
@@ -30,11 +28,6 @@ func (p *Profile) ApplyPollAPIKeyResponse(resp *hookdeck.PollAPIKeyResponse, gue
3028
p.ProjectMode = resp.ProjectMode
3129
p.ProjectType = ModeToProjectType(resp.ProjectMode)
3230
p.GuestURL = guestURL
33-
if guestUserID != "" {
34-
p.GuestUserID = guestUserID
35-
} else if guestURL == "" {
36-
p.GuestUserID = ""
37-
}
3831
}
3932

4033
// ApplyCIClient applies credentials from hookdeck login --ci.
@@ -44,5 +37,4 @@ func (p *Profile) ApplyCIClient(ci hookdeck.CIClient) {
4437
p.ProjectMode = ci.ProjectMode
4538
p.ProjectType = ModeToProjectType(ci.ProjectMode)
4639
p.GuestURL = ""
47-
p.GuestUserID = ""
4840
}

pkg/config/profile_credentials_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestProfile_ApplyValidateAPIKeyResponse(t *testing.T) {
4242
func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
4343
t.Run("nil response is no-op", func(t *testing.T) {
4444
p := &Profile{APIKey: "k", ProjectId: "p"}
45-
p.ApplyPollAPIKeyResponse(nil, "", "")
45+
p.ApplyPollAPIKeyResponse(nil, "")
4646
require.Equal(t, "k", p.APIKey)
4747
require.Equal(t, "p", p.ProjectId)
4848
})
@@ -53,12 +53,11 @@ func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
5353
APIKey: "key_from_poll",
5454
ProjectID: "team_p",
5555
ProjectMode: "inbound",
56-
}, "https://guest", "guest_user_1")
56+
}, "https://guest")
5757
require.Equal(t, "key_from_poll", p.APIKey)
5858
require.Equal(t, "team_p", p.ProjectId)
5959
require.Equal(t, ProjectTypeGateway, p.ProjectType)
6060
require.Equal(t, "https://guest", p.GuestURL)
61-
require.Equal(t, "guest_user_1", p.GuestUserID)
6261
})
6362

6463
t.Run("clears guest URL when empty string passed", func(t *testing.T) {
@@ -67,7 +66,7 @@ func TestProfile_ApplyPollAPIKeyResponse(t *testing.T) {
6766
APIKey: "k123456789012",
6867
ProjectID: "t",
6968
ProjectMode: "inbound",
70-
}, "", "")
69+
}, "")
7170
require.Empty(t, p.GuestURL)
7271
})
7372
}

pkg/gateway/mcp/tool_login.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
1515

1616
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
17-
"github.com/hookdeck/hookdeck-cli/pkg/login"
1817
"github.com/hookdeck/hookdeck-cli/pkg/project"
1918
"github.com/hookdeck/hookdeck-cli/pkg/validators"
2019
)
@@ -113,7 +112,7 @@ func handleLogin(srv *Server) mcpsdk.ToolHandler {
113112
}
114113

115114
authClient := &hookdeck.Client{BaseURL: parsedBaseURL, TelemetryDisabled: cfg.TelemetryDisabled}
116-
session, err := authClient.StartLogin(login.BuildStartLoginInput(cfg, ""))
115+
session, err := authClient.StartLogin(cfg.DeviceName)
117116
if err != nil {
118117
return ErrorResult(fmt.Sprintf("Failed to start login: %s", err)), nil
119118
}
@@ -163,7 +162,7 @@ func handleLogin(srv *Server) mcpsdk.ToolHandler {
163162
}
164163

165164
// Persist credentials so future MCP sessions start authenticated.
166-
cfg.Profile.ApplyPollAPIKeyResponse(response, "", "")
165+
cfg.Profile.ApplyPollAPIKeyResponse(response, "")
167166

168167
cfg.SaveActiveProfileAfterLogin()
169168

pkg/hookdeck/auth.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type ValidateAPIKeyResponse struct {
2121
UserID string `json:"user_id"`
2222
UserName string `json:"user_name"`
2323
UserEmail string `json:"user_email"`
24+
UserIsGuest bool `json:"user_is_guest"`
2425
OrganizationName string `json:"organization_name"`
2526
OrganizationID string `json:"organization_id"`
2627
ProjectID string `json:"team_id"`
@@ -49,13 +50,6 @@ type UpdateClientInput struct {
4950
DeviceName string `json:"device_name"`
5051
}
5152

52-
// StartLoginInput configures POST /cli-auth for browser login.
53-
type StartLoginInput struct {
54-
DeviceName string `json:"device_name"`
55-
GuestUserID string `json:"guest_user_id,omitempty"`
56-
GuestAPIKey string `json:"guest_api_key,omitempty"`
57-
}
58-
5953
// LoginSession represents an in-progress login flow
6054
type LoginSession struct {
6155
BrowserURL string
@@ -64,15 +58,20 @@ type LoginSession struct {
6458

6559
// GuestSession represents an in-progress guest login flow
6660
type GuestSession struct {
67-
BrowserURL string
68-
GuestURL string
69-
GuestUserID string
70-
pollURL string
61+
BrowserURL string
62+
GuestURL string
63+
pollURL string
7164
}
7265

7366
// StartLogin initiates the login flow and returns a session to wait for completion
74-
func (c *Client) StartLogin(input StartLoginInput) (*LoginSession, error) {
75-
jsonData, err := json.Marshal(input)
67+
func (c *Client) StartLogin(deviceName string) (*LoginSession, error) {
68+
data := struct {
69+
DeviceName string `json:"device_name"`
70+
}{
71+
DeviceName: deviceName,
72+
}
73+
74+
jsonData, err := json.Marshal(data)
7675
if err != nil {
7776
return nil, err
7877
}
@@ -106,17 +105,17 @@ func (c *Client) StartLogin(input StartLoginInput) (*LoginSession, error) {
106105
// StartGuestLogin initiates a guest login flow and returns a session to wait for completion
107106
func (c *Client) StartGuestLogin(deviceName string) (*GuestSession, error) {
108107
guest, err := c.CreateGuestUser(CreateGuestUserInput{
109-
DeviceName: deviceName,
108+
DeviceName: deviceName,
109+
LinkContext: "signup",
110110
})
111111
if err != nil {
112112
return nil, err
113113
}
114114

115115
return &GuestSession{
116-
BrowserURL: guest.BrowserURL,
117-
GuestURL: guest.Url,
118-
GuestUserID: guest.Id,
119-
pollURL: guest.PollURL,
116+
BrowserURL: guest.BrowserURL,
117+
GuestURL: guest.Url,
118+
pollURL: guest.PollURL,
120119
}, nil
121120
}
122121

pkg/hookdeck/guest.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ type GuestSigninLinkResponse struct {
2323
}
2424

2525
type CreateGuestUserInput struct {
26-
DeviceName string `json:"device_name"`
26+
DeviceName string `json:"device_name"`
27+
LinkContext string `json:"link_context,omitempty"`
2728
}
2829

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

4647
func (c *Client) RefreshGuestSigninLink() (GuestSigninLinkResponse, error) {
47-
res, err := c.Post(context.Background(), APIPathPrefix+"/cli/guest/signin-link", nil, nil)
48+
input_bytes, err := json.Marshal(CreateGuestUserInput{LinkContext: "signup"})
49+
if err != nil {
50+
return GuestSigninLinkResponse{}, err
51+
}
52+
res, err := c.Post(context.Background(), APIPathPrefix+"/cli/guest", input_bytes, nil)
4853
if err != nil {
4954
return GuestSigninLinkResponse{}, err
5055
}
Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
1-
package hookdeck
2-
3-
import (
4-
"net/http"
5-
"testing"
6-
7-
"github.com/stretchr/testify/require"
8-
)
9-
10-
func TestRedactHeadersForLog_redactsAuthorization(t *testing.T) {
11-
headers := http.Header{}
12-
headers.Set("Authorization", "Basic c2tfdGVzdDo=")
13-
headers.Set("Content-Type", "application/json")
14-
15-
redacted := redactHeadersForLog(headers)
16-
require.Equal(t, "[redacted]", redacted.Get("Authorization"))
17-
require.Equal(t, "application/json", redacted.Get("Content-Type"))
18-
require.Equal(t, "Basic c2tfdGVzdDo=", headers.Get("Authorization"))
19-
}
20-
21-
func TestRedactRequestBodyForLog_redactsGuestAPIKey(t *testing.T) {
22-
body := `{"device_name":"laptop","guest_user_id":"usr_1","guest_api_key":"hk_secret"}`
23-
redacted := redactRequestBodyForLog(body)
24-
require.Contains(t, redacted, `"guest_api_key":"[redacted]"`)
25-
require.Contains(t, redacted, `"guest_user_id":"usr_1"`)
26-
require.NotContains(t, redacted, "hk_secret")
27-
}
28-
29-
func TestRedactRequestBodyForLog_leavesNonJSONUnchanged(t *testing.T) {
30-
body := "guest_api_key=hk_secret"
31-
require.Equal(t, body, redactRequestBodyForLog(body))
32-
}
1+
package hookdeck
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestRedactHeadersForLog_redactsAuthorization(t *testing.T) {
11+
headers := http.Header{}
12+
headers.Set("Authorization", "Basic c2tfdGVzdDo=")
13+
headers.Set("Content-Type", "application/json")
14+
15+
redacted := redactHeadersForLog(headers)
16+
require.Equal(t, "[redacted]", redacted.Get("Authorization"))
17+
require.Equal(t, "application/json", redacted.Get("Content-Type"))
18+
require.Equal(t, "Basic c2tfdGVzdDo=", headers.Get("Authorization"))
19+
}
20+
21+
func TestRedactRequestBodyForLog_redactsGuestAPIKey(t *testing.T) {
22+
body := `{"device_name":"laptop","guest_api_key":"hk_secret"}`
23+
redacted := redactRequestBodyForLog(body)
24+
require.Contains(t, redacted, `"guest_api_key":"[redacted]"`)
25+
require.NotContains(t, redacted, "hk_secret")
26+
}
27+
28+
func TestRedactRequestBodyForLog_leavesNonJSONUnchanged(t *testing.T) {
29+
body := "guest_api_key=hk_secret"
30+
require.Equal(t, body, redactRequestBodyForLog(body))
31+
}

0 commit comments

Comments
 (0)