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
48 changes: 41 additions & 7 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,21 @@ hookdeck connection create \
--destination-url "https://api.example.com/stripe"
```

**4. Destination with Bearer Token**
**4. Destination with Hookdeck Signature (Default)**
```bash
# Hookdeck automatically signs outgoing webhooks - no configuration needed
hookdeck connection create \
--source-name "stripe-webhooks" \
--source-type STRIPE \
--source-webhook-secret "whsec_stripe_secret" \
--destination-name "api-with-verification" \
--destination-type HTTP \
--destination-url "https://api.example.com/webhook" \
--destination-auth-method hookdeck
```
*Note: Hookdeck Signature authentication is the default. Hookdeck automatically signs all outgoing webhooks with a signature that can be verified using Hookdeck's verification libraries. No webhook secret needs to be configured.*

**5. Destination with Bearer Token**
```bash
hookdeck connection create \
--source-name "github-webhooks" \
Expand All @@ -985,9 +999,11 @@ hookdeck connection create \
--destination-name "ci-system" \
--destination-type HTTP \
--destination-url "https://ci.example.com/webhook" \
--destination-auth-method bearer \
--destination-bearer-token "bearer_token_xyz"
```

**5. Source with Custom Response and Allowed HTTP Methods**
**6. Source with Custom Response and Allowed HTTP Methods**
```bash
hookdeck connection create \
--source-name "api-webhooks" \
Expand All @@ -1002,7 +1018,7 @@ hookdeck connection create \

#### Rule Configuration Examples

**6. Retry Rules**
**7. Retry Rules**
```bash
hookdeck connection create \
--source-name "payment-webhooks" \
Expand All @@ -1015,7 +1031,7 @@ hookdeck connection create \
--rule-retry-interval 60000
```

**7. Filter Rules**
**8. Filter Rules**
```bash
hookdeck connection create \
--source-name "events" \
Expand All @@ -1026,7 +1042,7 @@ hookdeck connection create \
--rule-filter-body '{"event_type":"payment.succeeded"}'
```

**8. All Rule Types Combined**
**9. All Rule Types Combined**
```bash
hookdeck connection create \
--source-name "shopify-webhooks" \
Expand All @@ -1042,7 +1058,7 @@ hookdeck connection create \
--rule-delay 5000
```

**9. Rate Limiting**
**10. Rate Limiting**
```bash
hookdeck connection create \
--source-name "high-volume-source" \
Expand All @@ -1054,6 +1070,19 @@ hookdeck connection create \
--destination-rate-limit-period minute
```

**11. GCP Service Account Authentication**
```bash
hookdeck connection create \
--source-name "webhooks" \
--source-type HTTP \
--destination-name "gcp-cloud-function" \
--destination-type HTTP \
--destination-url "https://us-central1-project-id.cloudfunctions.net/function" \
--destination-auth-method gcp \
--destination-gcp-service-account-key '{"type":"service_account","project_id":"project-id","private_key_id":"key-id","private_key":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n","client_email":"service-account@project-id.iam.gserviceaccount.com"}' \
--destination-gcp-scope "https://www.googleapis.com/auth/cloud-platform"
```

#### Available Flags

**Connection Configuration:**
Expand Down Expand Up @@ -1084,7 +1113,7 @@ hookdeck connection create \
- `--destination-cli-path <path>` - CLI path (default: `/`)
- `--destination-path-forwarding-disabled <true|false>` - Disable path forwarding for HTTP destinations (default: false)
- `--destination-http-method <method>` - HTTP method for HTTP destinations: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
- `--destination-auth-method <method>` - Authentication method: `hookdeck`, `bearer`, `basic`, `api_key`, `custom_signature`, `oauth2_client_credentials`, `oauth2_authorization_code`, `aws`
- `--destination-auth-method <method>` - Authentication method: `hookdeck`, `bearer`, `basic`, `api_key`, `custom_signature`, `oauth2_client_credentials`, `oauth2_authorization_code`, `aws`, `gcp`
- `--destination-rate-limit <number>` - Rate limit (requests per period)
- `--destination-rate-limit-period <period>` - Period: `second`, `minute`, `hour`, `day`, `month`, `year`

Expand Down Expand Up @@ -1136,6 +1165,11 @@ hookdeck connection create \
- `--destination-aws-region <region>` - AWS region
- `--destination-aws-service <service>` - AWS service name

*GCP Service Account:*
- `--destination-auth-method gcp`
- `--destination-gcp-service-account-key <json>` - GCP service account key JSON
- `--destination-gcp-scope <scope>` - GCP scope (optional)

**Rules - Retry:**
- `--rule-retry-strategy <strategy>` - Strategy: `linear`, `exponential`
- `--rule-retry-count <number>` - Number of retry attempts (1-20)
Expand Down
49 changes: 49 additions & 0 deletions pkg/cmd/connection_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,55 @@ func TestBuildAuthConfig(t *testing.T) {
wantErr: true,
errContains: "--destination-aws-region is required",
},
{
name: "gcp service account auth - valid",
setup: func(cc *connectionCreateCmd) {
cc.DestinationAuthMethod = "gcp"
cc.DestinationGCPServiceAccountKey = `{"type":"service_account","project_id":"test"}`
cc.DestinationGCPScope = "https://www.googleapis.com/auth/cloud-platform"
},
wantType: "GCP_SERVICE_ACCOUNT",
wantErr: false,
validate: func(t *testing.T, config map[string]interface{}) {
if config["type"] != "GCP_SERVICE_ACCOUNT" {
t.Errorf("expected type GCP_SERVICE_ACCOUNT, got %v", config["type"])
}
if config["service_account_key"] == "" {
t.Error("expected service_account_key to be set")
}
if config["scope"] != "https://www.googleapis.com/auth/cloud-platform" {
t.Errorf("expected scope, got %v", config["scope"])
}
},
},
{
name: "gcp service account auth - valid without scope",
setup: func(cc *connectionCreateCmd) {
cc.DestinationAuthMethod = "gcp"
cc.DestinationGCPServiceAccountKey = `{"type":"service_account","project_id":"test"}`
},
wantType: "GCP_SERVICE_ACCOUNT",
wantErr: false,
validate: func(t *testing.T, config map[string]interface{}) {
if config["type"] != "GCP_SERVICE_ACCOUNT" {
t.Errorf("expected type GCP_SERVICE_ACCOUNT, got %v", config["type"])
}
if config["service_account_key"] == "" {
t.Error("expected service_account_key to be set")
}
if _, hasScope := config["scope"]; hasScope {
t.Error("expected scope to not be set when not provided")
}
},
},
{
name: "gcp service account auth - missing key",
setup: func(cc *connectionCreateCmd) {
cc.DestinationAuthMethod = "gcp"
},
wantErr: true,
errContains: "--destination-gcp-service-account-key is required",
},
{
name: "unsupported auth method",
setup: func(cc *connectionCreateCmd) {
Expand Down
24 changes: 22 additions & 2 deletions pkg/cmd/connection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ type connectionCreateCmd struct {
DestinationAWSRegion string
DestinationAWSService string

// GCP Service Account flags
DestinationGCPServiceAccountKey string
DestinationGCPScope string

// Destination rate limiting flags
DestinationRateLimit int
DestinationRateLimitPeriod string
Expand Down Expand Up @@ -207,7 +211,7 @@ func newConnectionCreateCmd() *connectionCreateCmd {
cc.cmd.Flags().StringVar(&cc.destinationHTTPMethod, "destination-http-method", "", "HTTP method for HTTP destinations (GET, POST, PUT, PATCH, DELETE)")

// Destination authentication flags
cc.cmd.Flags().StringVar(&cc.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)")
cc.cmd.Flags().StringVar(&cc.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)")

// Bearer Token
cc.cmd.Flags().StringVar(&cc.DestinationBearerToken, "destination-bearer-token", "", "Bearer token for destination authentication")
Expand Down Expand Up @@ -241,6 +245,10 @@ func newConnectionCreateCmd() *connectionCreateCmd {
cc.cmd.Flags().StringVar(&cc.DestinationAWSRegion, "destination-aws-region", "", "AWS region")
cc.cmd.Flags().StringVar(&cc.DestinationAWSService, "destination-aws-service", "", "AWS service name")

// GCP Service Account
cc.cmd.Flags().StringVar(&cc.DestinationGCPServiceAccountKey, "destination-gcp-service-account-key", "", "GCP service account key JSON for destination authentication")
cc.cmd.Flags().StringVar(&cc.DestinationGCPScope, "destination-gcp-scope", "", "GCP scope for service account authentication")

// Destination rate limiting flags
cc.cmd.Flags().IntVar(&cc.DestinationRateLimit, "destination-rate-limit", 0, "Rate limit for destination (requests per period)")
cc.cmd.Flags().StringVar(&cc.DestinationRateLimitPeriod, "destination-rate-limit-period", "", "Rate limit period (second, minute, hour, concurrent)")
Expand Down Expand Up @@ -790,8 +798,20 @@ func (cc *connectionCreateCmd) buildAuthConfig() (map[string]interface{}, error)
authConfig["region"] = cc.DestinationAWSRegion
authConfig["service"] = cc.DestinationAWSService

case "gcp":
// GCP_SERVICE_ACCOUNT
if cc.DestinationGCPServiceAccountKey == "" {
return nil, fmt.Errorf("--destination-gcp-service-account-key is required for gcp auth method")
}
authConfig["type"] = "GCP_SERVICE_ACCOUNT"
authConfig["service_account_key"] = cc.DestinationGCPServiceAccountKey

if cc.DestinationGCPScope != "" {
authConfig["scope"] = cc.DestinationGCPScope
}

default:
return nil, fmt.Errorf("unsupported destination authentication method: %s (supported: hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)", cc.DestinationAuthMethod)
return nil, fmt.Errorf("unsupported destination authentication method: %s (supported: hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)", cc.DestinationAuthMethod)
}

return authConfig, nil
Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/connection_upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func newConnectionUpsertCmd() *connectionUpsertCmd {
cu.cmd.Flags().StringVar(&cu.destinationHTTPMethod, "destination-http-method", "", "HTTP method for HTTP destinations (GET, POST, PUT, PATCH, DELETE)")

// Destination authentication flags
cu.cmd.Flags().StringVar(&cu.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)")
cu.cmd.Flags().StringVar(&cu.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)")

// Bearer Token
cu.cmd.Flags().StringVar(&cu.DestinationBearerToken, "destination-bearer-token", "", "Bearer token for destination authentication")
Expand Down Expand Up @@ -150,6 +150,10 @@ func newConnectionUpsertCmd() *connectionUpsertCmd {
cu.cmd.Flags().StringVar(&cu.DestinationAWSRegion, "destination-aws-region", "", "AWS region")
cu.cmd.Flags().StringVar(&cu.DestinationAWSService, "destination-aws-service", "", "AWS service name")

// GCP Service Account
cu.cmd.Flags().StringVar(&cu.DestinationGCPServiceAccountKey, "destination-gcp-service-account-key", "", "GCP service account key JSON for destination authentication")
cu.cmd.Flags().StringVar(&cu.DestinationGCPScope, "destination-gcp-scope", "", "GCP scope for service account authentication")

// Destination rate limiting flags
cu.cmd.Flags().IntVar(&cu.DestinationRateLimit, "destination-rate-limit", 0, "Rate limit for destination (requests per period)")
cu.cmd.Flags().StringVar(&cu.DestinationRateLimitPeriod, "destination-rate-limit-period", "", "Rate limit period (second, minute, hour, concurrent)")
Expand Down
58 changes: 58 additions & 0 deletions test/acceptance/connection_oauth_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,62 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {

t.Logf("Successfully tested HTTP destination with AWS Signature: %s", connID)
})

t.Run("HTTP_Destination_GCP_ServiceAccount", func(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-gcp-sa-conn-" + timestamp
sourceName := "test-gcp-sa-source-" + timestamp
destName := "test-gcp-sa-dest-" + timestamp
destURL := "https://api.hookdeck.com/dev/null"

// Create connection with HTTP destination (GCP Service Account)
// Using a minimal but valid JSON structure for service account key
serviceAccountKey := `{"type":"service_account","project_id":"test-project","private_key_id":"test-key-id","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----\n","client_email":"test@test-project.iam.gserviceaccount.com","client_id":"123456789","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token"}`

stdout, stderr, err := cli.Run("connection", "create",
"--name", connName,
"--source-type", "WEBHOOK",
"--source-name", sourceName,
"--destination-type", "HTTP",
"--destination-name", destName,
"--destination-url", destURL,
"--destination-auth-method", "gcp",
"--destination-gcp-service-account-key", serviceAccountKey,
"--destination-gcp-scope", "https://www.googleapis.com/auth/cloud-platform",
"--output", "json")
require.NoError(t, err, "Failed to create connection: stderr=%s", stderr)

var createResp map[string]interface{}
err = json.Unmarshal([]byte(stdout), &createResp)
require.NoError(t, err, "Failed to parse creation response: %s", stdout)

connID, ok := createResp["id"].(string)
require.True(t, ok && connID != "", "Expected connection ID in creation response")

// Verify destination auth configuration
dest, ok := createResp["destination"].(map[string]interface{})
require.True(t, ok, "Expected destination object in creation response")

destConfig, ok := dest["config"].(map[string]interface{})
require.True(t, ok, "Expected destination config object")

if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
assert.Equal(t, "GCP_SERVICE_ACCOUNT", authMethod["type"], "Auth type should be GCP_SERVICE_ACCOUNT")
assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", authMethod["scope"], "GCP scope should match")
// Service account key should not be returned for security reasons
}

// Cleanup
t.Cleanup(func() {
deleteConnection(t, cli, connID)
})

t.Logf("Successfully tested HTTP destination with GCP Service Account: %s", connID)
})
}
4 changes: 2 additions & 2 deletions test/acceptance/connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ func TestConnectionWithDeduplicateRule(t *testing.T) {
"--destination-name", destName,
"--destination-type", "CLI",
"--destination-cli-path", "/webhooks",
"--rule-deduplicate-window", "86400",
"--rule-deduplicate-window", "60000",
"--rule-deduplicate-include-fields", "body.id,body.timestamp",
)
require.NoError(t, err, "Should create connection with deduplicate rule")
Expand All @@ -1344,7 +1344,7 @@ func TestConnectionWithDeduplicateRule(t *testing.T) {

rule := getConn.Rules[0]
assert.Equal(t, "deduplicate", rule["type"], "Rule type should be deduplicate")
assert.Equal(t, float64(86400), rule["window"], "Deduplicate window should be 86400 milliseconds")
assert.Equal(t, float64(60000), rule["window"], "Deduplicate window should be 60000 milliseconds (60 seconds)")

// Verify include_fields is correctly set and matches our input
if includeFields, ok := rule["include_fields"].([]interface{}); ok {
Expand Down