Skip to content

Commit 0149cba

Browse files
leggetterclaudecursoragent
committed
refactor(cli): align project-scoped credential errors with API
Rename ErrProjectScopedCredentials (ErrCIScopedCredentials alias), update messages for single-project scope, and match CLI_PROJECT_SCOPED from API. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1282b2e commit 0149cba

7 files changed

Lines changed: 119 additions & 113 deletions

File tree

pkg/gateway/mcp/tool_projects_errors.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ func listProjectsFailureMessage(err error) string {
2020
}
2121

2222
func shouldSuggestReauthAfterListProjectsFailure(err error) bool {
23-
if errors.Is(err, project.ErrCIScopedCredentials) {
23+
if errors.Is(err, project.ErrProjectScopedCredentials) {
2424
return true
2525
}
2626
var apiErr *hookdeck.APIError
2727
if errors.As(err, &apiErr) {
2828
if apiErr.StatusCode == http.StatusForbidden || apiErr.StatusCode == http.StatusUnauthorized {
2929
return true
3030
}
31-
if strings.Contains(strings.ToUpper(apiErr.Message), "CLI_USER_REQUIRED") {
31+
msg := strings.ToUpper(apiErr.Message)
32+
if strings.Contains(msg, "CLI_PROJECT_SCOPED") || strings.Contains(msg, "CLI_USER_REQUIRED") {
3233
return true
3334
}
3435
return strings.Contains(strings.ToLower(apiErr.Message), "fatal")

pkg/gateway/mcp/tool_projects_errors_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ func TestShouldSuggestReauthAfterListProjectsFailure(t *testing.T) {
1616
want bool
1717
}{
1818
{
19-
name: "CI scoped credentials",
20-
err: project.ErrCIScopedCredentials,
19+
name: "project-scoped credentials",
20+
err: project.ErrProjectScopedCredentials,
2121
want: true,
2222
},
2323
{
24-
name: "APIError 403 CLI_USER_REQUIRED",
25-
err: &hookdeck.APIError{StatusCode: 403, Message: "CLI_USER_REQUIRED: listing projects requires login"},
24+
name: "APIError 403 CLI_PROJECT_SCOPED",
25+
err: &hookdeck.APIError{StatusCode: 403, Message: "CLI_PROJECT_SCOPED: cannot list all projects"},
2626
want: true,
2727
},
2828
{

pkg/login/client_login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func Login(config *configpkg.Config, input io.Reader) error {
6565
} else {
6666
ansi.StopSpinner(s, "", os.Stdout)
6767
if !stdinIsTerminal() {
68-
return project.ErrCIScopedCredentials
68+
return project.ErrProjectScopedCredentials
6969
}
7070
fmt.Fprintln(os.Stdout, "Your saved key is scoped to a single project (CI). Starting browser sign-in...")
7171
config.Profile.APIKey = ""

pkg/login/client_login_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ api_key = "hk_test_cikey_abcdefghij"
164164
cfg.TelemetryDisabled = true
165165

166166
err = Login(cfg, strings.NewReader("\n"))
167-
require.ErrorIs(t, err, project.ErrCIScopedCredentials)
167+
require.ErrorIs(t, err, project.ErrProjectScopedCredentials)
168168
require.False(t, sawCLIAuthPost)
169169
}
170170

pkg/project/credentials.go

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,65 @@
1-
package project
2-
3-
import (
4-
"errors"
5-
"fmt"
6-
7-
"github.com/hookdeck/hookdeck-cli/pkg/config"
8-
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
9-
)
10-
11-
// ErrCIScopedCredentials is returned when the stored CLI key is valid but not
12-
// associated with a user (for example after hookdeck ci).
13-
var ErrCIScopedCredentials = errors.New(
14-
"listing projects requires a user-associated CLI key; keys from hookdeck ci are scoped to a single project; " +
15-
"run hookdeck login in an interactive terminal, hookdeck login --cli-key with a product CLI key, or hookdeck_login via MCP for full account access",
16-
)
17-
18-
// ValidateCredentials calls GET /cli-auth/validate without sending X-Team-ID.
19-
func ValidateCredentials(config *config.Config) (*hookdeck.ValidateAPIKeyResponse, error) {
20-
return config.GetAPIClient().ValidateAPIKey()
21-
}
22-
23-
// EnsureUserAssociatedCredentials rejects CI-scoped keys before cross-project calls.
24-
func EnsureUserAssociatedCredentials(config *config.Config) error {
25-
response, err := ValidateCredentials(config)
26-
if err != nil {
27-
return err
28-
}
29-
if response.UserID == "" {
30-
return ErrCIScopedCredentials
31-
}
32-
return nil
33-
}
34-
35-
// EnsureUserAssociatedClient rejects CI-scoped keys for an in-memory API client (MCP).
36-
func EnsureUserAssociatedClient(client *hookdeck.Client) error {
37-
if client == nil || client.APIKey == "" {
38-
return fmt.Errorf("not authenticated")
39-
}
40-
response, err := client.ValidateAPIKey()
41-
if err != nil {
42-
return err
43-
}
44-
if response.UserID == "" {
45-
return ErrCIScopedCredentials
46-
}
47-
return nil
48-
}
49-
50-
// CredentialsLackUserAssociation reports whether validate succeeded without a user_id.
51-
func CredentialsLackUserAssociation(client *hookdeck.Client) (bool, error) {
52-
if client == nil || client.APIKey == "" {
53-
return false, nil
54-
}
55-
response, err := client.ValidateAPIKey()
56-
if err != nil {
57-
return false, err
58-
}
59-
return response.UserID == "", nil
60-
}
1+
package project
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/hookdeck/hookdeck-cli/pkg/config"
8+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
9+
)
10+
11+
// ErrProjectScopedCredentials is returned when the stored CLI key is valid but
12+
// limited to a single project (for example after hookdeck ci).
13+
var ErrProjectScopedCredentials = errors.New(
14+
"this credential is scoped to a single project and cannot list all projects; " +
15+
"keys from hookdeck ci are project-scoped; " +
16+
"run hookdeck login in an interactive terminal, hookdeck login --cli-key with a product CLI key, or hookdeck_login via MCP for account-wide access",
17+
)
18+
19+
// ErrCIScopedCredentials is an alias for ErrProjectScopedCredentials.
20+
var ErrCIScopedCredentials = ErrProjectScopedCredentials
21+
22+
// ValidateCredentials calls GET /cli-auth/validate without sending X-Team-ID.
23+
func ValidateCredentials(config *config.Config) (*hookdeck.ValidateAPIKeyResponse, error) {
24+
return config.GetAPIClient().ValidateAPIKey()
25+
}
26+
27+
// EnsureUserAssociatedCredentials rejects project-scoped keys before cross-project calls.
28+
func EnsureUserAssociatedCredentials(config *config.Config) error {
29+
response, err := ValidateCredentials(config)
30+
if err != nil {
31+
return err
32+
}
33+
if response.UserID == "" {
34+
return ErrProjectScopedCredentials
35+
}
36+
return nil
37+
}
38+
39+
// EnsureUserAssociatedClient rejects project-scoped keys for an in-memory API client (MCP).
40+
func EnsureUserAssociatedClient(client *hookdeck.Client) error {
41+
if client == nil || client.APIKey == "" {
42+
return fmt.Errorf("not authenticated")
43+
}
44+
response, err := client.ValidateAPIKey()
45+
if err != nil {
46+
return err
47+
}
48+
if response.UserID == "" {
49+
return ErrProjectScopedCredentials
50+
}
51+
return nil
52+
}
53+
54+
// CredentialsLackUserAssociation reports whether validate succeeded for a project-scoped key
55+
// (no user_id — our proxy for single-project credential scope today).
56+
func CredentialsLackUserAssociation(client *hookdeck.Client) (bool, error) {
57+
if client == nil || client.APIKey == "" {
58+
return false, nil
59+
}
60+
response, err := client.ValidateAPIKey()
61+
if err != nil {
62+
return false, err
63+
}
64+
return response.UserID == "", nil
65+
}

pkg/project/credentials_test.go

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
1-
package project
2-
3-
import (
4-
"encoding/json"
5-
"net/http"
6-
"net/http/httptest"
7-
"testing"
8-
9-
configpkg "github.com/hookdeck/hookdeck-cli/pkg/config"
10-
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
11-
"github.com/stretchr/testify/require"
12-
)
13-
14-
func TestEnsureUserAssociatedCredentials_rejectsCIKey(t *testing.T) {
15-
configpkg.ResetAPIClientForTesting()
16-
t.Cleanup(configpkg.ResetAPIClientForTesting)
17-
18-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19-
require.Equal(t, hookdeck.APIPathPrefix+"/cli-auth/validate", r.URL.Path)
20-
w.Header().Set("Content-Type", "application/json")
21-
_ = json.NewEncoder(w).Encode(hookdeck.ValidateAPIKeyResponse{
22-
ProjectID: "tm_ci",
23-
ProjectMode: "inbound",
24-
OrganizationName: "Org",
25-
OrganizationID: "org_1",
26-
ProjectName: "CI Project",
27-
})
28-
}))
29-
t.Cleanup(ts.Close)
30-
31-
cfg := &configpkg.Config{
32-
APIBaseURL: ts.URL,
33-
LogLevel: "error",
34-
TelemetryDisabled: true,
35-
}
36-
cfg.Profile = configpkg.Profile{
37-
Name: "default",
38-
APIKey: "hk_test_ci_key_abcdefghij",
39-
Config: cfg,
40-
}
41-
42-
err := EnsureUserAssociatedCredentials(cfg)
43-
require.ErrorIs(t, err, ErrCIScopedCredentials)
44-
}
1+
package project
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
configpkg "github.com/hookdeck/hookdeck-cli/pkg/config"
10+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestEnsureUserAssociatedCredentials_rejectsProjectScopedKey(t *testing.T) {
15+
configpkg.ResetAPIClientForTesting()
16+
t.Cleanup(configpkg.ResetAPIClientForTesting)
17+
18+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
require.Equal(t, hookdeck.APIPathPrefix+"/cli-auth/validate", r.URL.Path)
20+
w.Header().Set("Content-Type", "application/json")
21+
_ = json.NewEncoder(w).Encode(hookdeck.ValidateAPIKeyResponse{
22+
ProjectID: "tm_ci",
23+
ProjectMode: "inbound",
24+
OrganizationName: "Org",
25+
OrganizationID: "org_1",
26+
ProjectName: "CI Project",
27+
})
28+
}))
29+
t.Cleanup(ts.Close)
30+
31+
cfg := &configpkg.Config{
32+
APIBaseURL: ts.URL,
33+
LogLevel: "error",
34+
TelemetryDisabled: true,
35+
}
36+
cfg.Profile = configpkg.Profile{
37+
Name: "default",
38+
APIKey: "hk_test_ci_key_abcdefghij",
39+
Config: cfg,
40+
}
41+
42+
err := EnsureUserAssociatedCredentials(cfg)
43+
require.ErrorIs(t, err, ErrProjectScopedCredentials)
44+
}

test/acceptance/project_use_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,5 +346,5 @@ func TestProjectListFailsWithCIKeyAcceptance(t *testing.T) {
346346
combined := stdout + stderr
347347
assert.NotContains(t, combined, "Fatal Error")
348348
assert.NotContains(t, combined, "status=500")
349-
assert.Contains(t, combined, "user-associated CLI key")
349+
assert.Contains(t, combined, "single project")
350350
}

0 commit comments

Comments
 (0)