package webhook import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "time" ) // Client handles webhook operations type Client struct { config Config httpClient *http.Client } // Payload represents the webhook payload type Payload struct { Email string `json:"email"` Domain string `json:"domain"` Action string `json:"action"` Timestamp time.Time `json:"timestamp"` } // New creates a new webhook client with the provided configuration func New(cfg Config) (*Client, error) { if cfg.BaseURL == "" || cfg.Domain == "" || cfg.SecretKey == "" { return nil, ErrInvalidConfig } // Apply defaults if not set if cfg.Timeout == 0 { cfg.Timeout = DefaultConfig().Timeout } if cfg.MaxRetries == 0 { cfg.MaxRetries = DefaultConfig().MaxRetries } if cfg.RetryBackoff == 0 { cfg.RetryBackoff = DefaultConfig().RetryBackoff } return &Client{ config: cfg, httpClient: &http.Client{ Timeout: cfg.Timeout, }, }, nil } // Send sends a webhook with the provided email and action func (c *Client) Send(ctx context.Context, email, action string) error { payload := Payload{ Email: email, Domain: c.config.Domain, Action: action, Timestamp: time.Now(), } payloadBytes, err := json.Marshal(payload) if err != nil { return NewError("marshal", ErrPayloadMarshall, err.Error()) } // Calculate signature signature := c.calculateHMAC(payloadBytes) // Create request req, err := http.NewRequestWithContext( ctx, http.MethodPost, fmt.Sprintf("%s/webhook/user-sync", c.config.BaseURL), bytes.NewBuffer(payloadBytes), ) if err != nil { return NewError("create_request", ErrRequestCreation, err.Error()) } // Set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Webhook-Signature", signature) req.Header.Set("X-Webhook-Domain", c.config.Domain) // Send request with retries var lastErr error for retry := 0; retry < c.config.MaxRetries; retry++ { select { case <-ctx.Done(): return ctx.Err() default: resp, err := c.httpClient.Do(req) if err != nil { lastErr = err time.Sleep(c.config.RetryBackoff * time.Duration(retry+1)) continue } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return nil } if resp.StatusCode == http.StatusTooManyRequests { lastErr = ErrTooManyRequests time.Sleep(c.config.RetryBackoff * time.Duration(retry+1)) continue } // Permanent failure if resp.StatusCode >= 400 && resp.StatusCode != http.StatusTooManyRequests { return NewHTTPError( "send", resp.StatusCode, ErrRequestFailed, "permanent failure", ) } } } if lastErr != nil { return NewError("send", ErrMaxRetriesReached, lastErr.Error()) } return NewError("send", ErrMaxRetriesReached, "all retry attempts failed") } // calculateHMAC calculates the HMAC signature for the payload func (c *Client) calculateHMAC(message []byte) string { mac := hmac.New(sha256.New, []byte(c.config.SecretKey)) mac.Write(message) return hex.EncodeToString(mac.Sum(nil)) } // Close closes the webhook client and its resources func (c *Client) Close() error { if c.httpClient != nil { if transport, ok := c.httpClient.Transport.(*http.Transport); ok { transport.CloseIdleConnections() } c.httpClient = nil } return nil } // ValidateSignature validates the HMAC signature of a webhook payload func ValidateSignature(payload []byte, signature, secretKey string) bool { mac := hmac.New(sha256.New, []byte(secretKey)) mac.Write(payload) expectedMAC := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedMAC)) }