Skip to content

Commit 4ba1f0b

Browse files
fix: correct destination auth config structure and add --include-destination-auth flag
The CLI was sending destination authentication credentials under an `auth_method` key, but the API expects `auth_type` (string) and `auth` (credentials map) as separate fields inside `destination.config`. Changes: - Fix buildDestinationConfig() to split auth into auth_type and auth fields - Add GetDestination API client method for fetching destinations directly - Add --include-destination-auth flag to `connection get` that fetches destination credentials via GET /destinations/{id}?include=config.auth (the connections API does not support this parameter) - Update acceptance tests to verify credentials are stored correctly by doing a `connection get --include-destination-auth` after creation - Update README.md and REFERENCE.md with new flag documentation Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b0cd334 commit 4ba1f0b

File tree

7 files changed

+189
-25
lines changed

7 files changed

+189
-25
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,9 @@ $ hookdeck connection get "my-connection"
841841

842842
# Get as JSON
843843
$ hookdeck connection get conn_123abc --output json
844+
845+
# Include destination authentication credentials
846+
$ hookdeck connection get conn_123abc --include-destination-auth --output json
844847
```
845848

846849
#### Connection lifecycle management

REFERENCE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,8 +940,16 @@ hookdeck connection get "my-connection"
940940

941941
# Get as JSON
942942
hookdeck connection get conn_abc123 --output json
943+
944+
# Include destination authentication credentials
945+
hookdeck connection get conn_abc123 --include-destination-auth --output json
943946
```
944947

948+
**Flags:**
949+
950+
- `--output json` - Output in JSON format
951+
- `--include-destination-auth` - Include destination authentication credentials in the response (fetches via GET /destinations/{id}?include=config.auth)
952+
945953
### Create Connection
946954

947955
Create a new connection with inline source/destination creation or by referencing existing resources.

pkg/cmd/connection_create.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,14 @@ func (cc *connectionCreateCmd) buildDestinationConfig() (map[string]interface{},
654654
}
655655

656656
if len(authConfig) > 0 {
657-
config["auth_method"] = authConfig
657+
config["auth_type"] = authConfig["type"]
658+
auth := make(map[string]interface{})
659+
for k, v := range authConfig {
660+
if k != "type" {
661+
auth[k] = v
662+
}
663+
}
664+
config["auth"] = auth
658665
}
659666

660667
// Add rate limiting configuration

pkg/cmd/connection_get.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import (
1717
type connectionGetCmd struct {
1818
cmd *cobra.Command
1919

20-
output string
20+
output string
21+
includeDestinationAuth bool
2122
}
2223

2324
func newConnectionGetCmd() *connectionGetCmd {
@@ -41,6 +42,7 @@ Examples:
4142
}
4243

4344
cc.cmd.Flags().StringVar(&cc.output, "output", "", "Output format (json)")
45+
addIncludeDestinationAuthFlag(cc.cmd, &cc.includeDestinationAuth)
4446

4547
return cc
4648
}
@@ -66,6 +68,17 @@ func (cc *connectionGetCmd) runConnectionGetCmd(cmd *cobra.Command, args []strin
6668
return formatConnectionError(err, connectionIDOrName)
6769
}
6870

71+
// The connections API does not support include=config.auth, so when
72+
// --include-destination-auth is requested we fetch the destination directly
73+
// from GET /destinations/{id}?include=config.auth and merge the enriched
74+
// config back into the connection response.
75+
if cc.includeDestinationAuth && conn.Destination != nil {
76+
dest, err := apiClient.GetDestination(ctx, conn.Destination.ID, includeAuthParams(true))
77+
if err == nil {
78+
conn.Destination = dest
79+
}
80+
}
81+
6982
if cc.output == "json" {
7083
jsonBytes, err := json.MarshalIndent(conn, "", " ")
7184
if err != nil {

pkg/cmd/connection_include.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmd
2+
3+
import "github.com/spf13/cobra"
4+
5+
// addIncludeDestinationAuthFlag registers the --include-destination-auth flag on a cobra command.
6+
// When set, the CLI fetches destination auth credentials via
7+
// GET /destinations/{id}?include=config.auth and merges them into the response.
8+
func addIncludeDestinationAuthFlag(cmd *cobra.Command, target *bool) {
9+
cmd.Flags().BoolVar(target, "include-destination-auth", false,
10+
"Include destination authentication credentials in the response")
11+
}
12+
13+
// includeAuthParams returns a map with the include query parameter set
14+
// if includeAuth is true, or nil otherwise.
15+
func includeAuthParams(includeAuth bool) map[string]string {
16+
if includeAuth {
17+
return map[string]string{"include": "config.auth"}
18+
}
19+
return nil
20+
}

pkg/hookdeck/destinations.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package hookdeck
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/url"
47
"time"
58
)
69

@@ -55,6 +58,27 @@ func (d *Destination) SetCLIPath(path string) {
5558
}
5659
}
5760

61+
// GetDestination retrieves a single destination by ID
62+
func (c *Client) GetDestination(ctx context.Context, id string, params map[string]string) (*Destination, error) {
63+
queryParams := url.Values{}
64+
for k, v := range params {
65+
queryParams.Add(k, v)
66+
}
67+
68+
resp, err := c.Get(ctx, fmt.Sprintf("/2025-07-01/destinations/%s", id), queryParams.Encode(), nil)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
var destination Destination
74+
_, err = postprocessJsonResponse(resp, &destination)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to parse destination response: %w", err)
77+
}
78+
79+
return &destination, nil
80+
}
81+
5882
// DestinationCreateInput represents input for creating a destination inline
5983
type DestinationCreateInput struct {
6084
Name string `json:"name"`

test/acceptance/connection_oauth_aws_test.go

Lines changed: 112 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,34 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {
5353
destConfig, ok := dest["config"].(map[string]interface{})
5454
require.True(t, ok, "Expected destination config object")
5555

56-
if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
57-
assert.Equal(t, "OAUTH2_CLIENT_CREDENTIALS", authMethod["type"], "Auth type should be OAUTH2_CLIENT_CREDENTIALS")
58-
assert.Equal(t, "https://auth.example.com/oauth/token", authMethod["auth_server"], "Auth server should match")
59-
assert.Equal(t, "client_123", authMethod["client_id"], "Client ID should match")
60-
// Client secret and scopes may or may not be returned depending on API
61-
}
56+
authType, ok := destConfig["auth_type"].(string)
57+
require.True(t, ok, "Expected auth_type string in destination config, got config: %v", destConfig)
58+
assert.Equal(t, "OAUTH2_CLIENT_CREDENTIALS", authType, "Auth type should be OAUTH2_CLIENT_CREDENTIALS")
59+
60+
// Fetch connection with --include-destination-auth to verify credentials were stored
61+
getStdout, getStderr, getErr := cli.Run("connection", "get", connID,
62+
"--include-destination-auth",
63+
"--output", "json")
64+
require.NoError(t, getErr, "Failed to get connection: stderr=%s", getStderr)
65+
66+
var getResp map[string]interface{}
67+
err = json.Unmarshal([]byte(getStdout), &getResp)
68+
require.NoError(t, err, "Failed to parse get response: %s", getStdout)
69+
70+
getDest, ok := getResp["destination"].(map[string]interface{})
71+
require.True(t, ok, "Expected destination in get response")
72+
getConfig, ok := getDest["config"].(map[string]interface{})
73+
require.True(t, ok, "Expected config in get response destination")
74+
75+
getAuthType, ok := getConfig["auth_type"].(string)
76+
require.True(t, ok, "Expected auth_type in get response config: %v", getConfig)
77+
assert.Equal(t, "OAUTH2_CLIENT_CREDENTIALS", getAuthType, "Auth type should match on get")
78+
79+
getAuth, ok := getConfig["auth"].(map[string]interface{})
80+
require.True(t, ok, "Expected auth object in get response config: %v", getConfig)
81+
assert.Equal(t, "https://auth.example.com/oauth/token", getAuth["auth_server"], "Auth server should match")
82+
assert.Equal(t, "client_123", getAuth["client_id"], "Client ID should match")
83+
assert.Equal(t, "secret_456", getAuth["client_secret"], "Client secret should match with --include-destination-auth")
6284

6385
// Cleanup
6486
t.Cleanup(func() {
@@ -112,12 +134,34 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {
112134
destConfig, ok := dest["config"].(map[string]interface{})
113135
require.True(t, ok, "Expected destination config object")
114136

115-
if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
116-
assert.Equal(t, "OAUTH2_AUTHORIZATION_CODE", authMethod["type"], "Auth type should be OAUTH2_AUTHORIZATION_CODE")
117-
assert.Equal(t, "https://auth.example.com/oauth/token", authMethod["auth_server"], "Auth server should match")
118-
assert.Equal(t, "client_789", authMethod["client_id"], "Client ID should match")
119-
// Sensitive fields like client_secret, refresh_token may not be returned
120-
}
137+
authType, ok := destConfig["auth_type"].(string)
138+
require.True(t, ok, "Expected auth_type string in destination config, got config: %v", destConfig)
139+
assert.Equal(t, "OAUTH2_AUTHORIZATION_CODE", authType, "Auth type should be OAUTH2_AUTHORIZATION_CODE")
140+
141+
// Fetch connection with --include-destination-auth to verify credentials were stored
142+
getStdout, getStderr, getErr := cli.Run("connection", "get", connID,
143+
"--include-destination-auth",
144+
"--output", "json")
145+
require.NoError(t, getErr, "Failed to get connection: stderr=%s", getStderr)
146+
147+
var getResp map[string]interface{}
148+
err = json.Unmarshal([]byte(getStdout), &getResp)
149+
require.NoError(t, err, "Failed to parse get response: %s", getStdout)
150+
151+
getDest, ok := getResp["destination"].(map[string]interface{})
152+
require.True(t, ok, "Expected destination in get response")
153+
getConfig, ok := getDest["config"].(map[string]interface{})
154+
require.True(t, ok, "Expected config in get response destination")
155+
156+
getAuthType, ok := getConfig["auth_type"].(string)
157+
require.True(t, ok, "Expected auth_type in get response config: %v", getConfig)
158+
assert.Equal(t, "OAUTH2_AUTHORIZATION_CODE", getAuthType, "Auth type should match on get")
159+
160+
getAuth, ok := getConfig["auth"].(map[string]interface{})
161+
require.True(t, ok, "Expected auth object in get response config: %v", getConfig)
162+
assert.Equal(t, "https://auth.example.com/oauth/token", getAuth["auth_server"], "Auth server should match")
163+
assert.Equal(t, "client_789", getAuth["client_id"], "Client ID should match")
164+
assert.Equal(t, "secret_abc", getAuth["client_secret"], "Client secret should match with --include-destination-auth")
121165

122166
// Cleanup
123167
t.Cleanup(func() {
@@ -170,12 +214,35 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {
170214
destConfig, ok := dest["config"].(map[string]interface{})
171215
require.True(t, ok, "Expected destination config object")
172216

173-
if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
174-
assert.Equal(t, "AWS_SIGNATURE", authMethod["type"], "Auth type should be AWS_SIGNATURE")
175-
assert.Equal(t, "us-east-1", authMethod["region"], "AWS region should match")
176-
assert.Equal(t, "execute-api", authMethod["service"], "AWS service should match")
177-
// Access key may be returned but secret key should not be for security
178-
}
217+
authType, ok := destConfig["auth_type"].(string)
218+
require.True(t, ok, "Expected auth_type string in destination config, got config: %v", destConfig)
219+
assert.Equal(t, "AWS_SIGNATURE", authType, "Auth type should be AWS_SIGNATURE")
220+
221+
// Fetch connection with --include-destination-auth to verify credentials were stored
222+
getStdout, getStderr, getErr := cli.Run("connection", "get", connID,
223+
"--include-destination-auth",
224+
"--output", "json")
225+
require.NoError(t, getErr, "Failed to get connection: stderr=%s", getStderr)
226+
227+
var getResp map[string]interface{}
228+
err = json.Unmarshal([]byte(getStdout), &getResp)
229+
require.NoError(t, err, "Failed to parse get response: %s", getStdout)
230+
231+
getDest, ok := getResp["destination"].(map[string]interface{})
232+
require.True(t, ok, "Expected destination in get response")
233+
getConfig, ok := getDest["config"].(map[string]interface{})
234+
require.True(t, ok, "Expected config in get response destination")
235+
236+
getAuthType, ok := getConfig["auth_type"].(string)
237+
require.True(t, ok, "Expected auth_type in get response config: %v", getConfig)
238+
assert.Equal(t, "AWS_SIGNATURE", getAuthType, "Auth type should match on get")
239+
240+
getAuth, ok := getConfig["auth"].(map[string]interface{})
241+
require.True(t, ok, "Expected auth object in get response config: %v", getConfig)
242+
assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", getAuth["access_key_id"], "AWS access key ID should match")
243+
assert.Equal(t, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", getAuth["secret_access_key"], "AWS secret access key should match")
244+
assert.Equal(t, "us-east-1", getAuth["region"], "AWS region should match")
245+
assert.Equal(t, "execute-api", getAuth["service"], "AWS service should match")
179246

180247
// Cleanup
181248
t.Cleanup(func() {
@@ -229,11 +296,33 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {
229296
destConfig, ok := dest["config"].(map[string]interface{})
230297
require.True(t, ok, "Expected destination config object")
231298

232-
if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
233-
assert.Equal(t, "GCP_SERVICE_ACCOUNT", authMethod["type"], "Auth type should be GCP_SERVICE_ACCOUNT")
234-
assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", authMethod["scope"], "GCP scope should match")
235-
// Service account key should not be returned for security reasons
236-
}
299+
authType, ok := destConfig["auth_type"].(string)
300+
require.True(t, ok, "Expected auth_type string in destination config, got config: %v", destConfig)
301+
assert.Equal(t, "GCP_SERVICE_ACCOUNT", authType, "Auth type should be GCP_SERVICE_ACCOUNT")
302+
303+
// Fetch connection with --include-destination-auth to verify credentials were stored
304+
getStdout, getStderr, getErr := cli.Run("connection", "get", connID,
305+
"--include-destination-auth",
306+
"--output", "json")
307+
require.NoError(t, getErr, "Failed to get connection: stderr=%s", getStderr)
308+
309+
var getResp map[string]interface{}
310+
err = json.Unmarshal([]byte(getStdout), &getResp)
311+
require.NoError(t, err, "Failed to parse get response: %s", getStdout)
312+
313+
getDest, ok := getResp["destination"].(map[string]interface{})
314+
require.True(t, ok, "Expected destination in get response")
315+
getConfig, ok := getDest["config"].(map[string]interface{})
316+
require.True(t, ok, "Expected config in get response destination")
317+
318+
getAuthType, ok := getConfig["auth_type"].(string)
319+
require.True(t, ok, "Expected auth_type in get response config: %v", getConfig)
320+
assert.Equal(t, "GCP_SERVICE_ACCOUNT", getAuthType, "Auth type should match on get")
321+
322+
getAuth, ok := getConfig["auth"].(map[string]interface{})
323+
require.True(t, ok, "Expected auth object in get response config: %v", getConfig)
324+
assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", getAuth["scope"], "GCP scope should match")
325+
assert.NotEmpty(t, getAuth["service_account_key"], "Service account key should be present with --include-destination-auth")
237326

238327
// Cleanup
239328
t.Cleanup(func() {

0 commit comments

Comments
 (0)