webhook

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package webhook implements outbound webhooks: endpoint configuration stored in gk_webhook, a durable delivery log in gk_webhook_delivery, HMAC-signed HTTP delivery with bounded retries, and the event-source cursors product code uses to turn database changes into events exactly once.

Index

Constants

View Source
const (
	StatusPending    = "pending"
	StatusDelivering = "delivering"
	StatusDelivered  = "delivered"
	StatusFailed     = "failed"
)

Delivery status values stored in gk_webhook_delivery.status.

View Source
const (
	SignatureHeader = "X-Webhook-Signature"
	EventHeader     = "X-Webhook-Event"
	DeliveryHeader  = "X-Webhook-Delivery"
	UserAgent       = "GoatFlow-Webhook/1.0"
)

HTTP headers set on every outbound delivery. The signature header carries "sha256=<hex HMAC-SHA256 of the raw body keyed by the webhook secret>", the format GoatFlow's own inbound plugin webhooks verify.

View Source
const (
	DefaultRetryCount     = 3
	MaxRetryCount         = 10
	DefaultTimeoutSeconds = 10
	MaxTimeoutSeconds     = 60
)

Limits and defaults for webhook configuration.

View Source
const AllowPrivateTargetsEnv = "GOATFLOW_WEBHOOK_ALLOW_PRIVATE_TARGETS"

AllowPrivateTargetsEnv names the environment variable that lets webhooks reach loopback, private, link-local and other internal addresses. It is an operator setting (not editable in the admin UI) because a webhook URL is otherwise a way for an admin account to make the server call internal services such as cloud metadata endpoints. Default: not allowed.

View Source
const MaxParallelWebhooks = 8

MaxParallelWebhooks bounds how many webhooks one ProcessDue call sends to at the same time. Deliveries to one webhook are sent one at a time, oldest first, so a slow or unreachable endpoint only delays its own deliveries.

View Source
const TestEvent = "webhook.test"

TestEvent is the event type of deliveries created by the test endpoint.

Variables

View Source
var ErrDuplicateName = errors.New("a webhook with this name already exists")

ErrDuplicateName is returned when another webhook already uses the name.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned when a webhook or delivery does not exist.

Functions

func AllowPrivateTargets added in v0.10.0

func AllowPrivateTargets() bool

AllowPrivateTargets reports whether AllowPrivateTargetsEnv is set to a true value ("true", "1", ...). It is read on every check, so tests and operators do not depend on process start order.

func CheckTargetHost added in v0.10.0

func CheckTargetHost(host string) error

CheckTargetHost validates the host of an admin-supplied URL (webhook, identity-provider metadata) when it is saved. Only IP literals and the always-loopback name "localhost" (RFC 6761) can be judged without DNS; other names are checked on every connection, after resolution, by targetDialer.

func NewHTTPClient added in v0.10.0

func NewHTTPClient(timeout time.Duration) *http.Client

NewHTTPClient returns a client with the delivery address guard (every resolved address checked and pinned, no redirects, no proxy) for other admin-supplied URLs the server fetches, such as identity-provider metadata and OIDC discovery documents. Requests give up after timeout.

func Sign added in v0.10.0

func Sign(secret string, body []byte) string

Sign returns the X-Webhook-Signature value for body: "sha256=<hex>".

func ValidateSecret added in v0.10.0

func ValidateSecret(secret string) error

ValidateSecret checks a signing secret supplied by an administrator. An empty secret means "unsigned".

Types

type BlockedTargetError added in v0.10.0

type BlockedTargetError struct {
	Host string
	Addr netip.Addr
}

BlockedTargetError is returned when a webhook host is, or resolves to, an address deliveries may not reach.

func (*BlockedTargetError) Error added in v0.10.0

func (e *BlockedTargetError) Error() string

type Delivery added in v0.10.0

type Delivery struct {
	ID            int64      `json:"id"`
	WebhookID     int64      `json:"webhook_id"`
	Event         string     `json:"event"`
	Payload       string     `json:"payload,omitempty"`
	Status        string     `json:"status"`
	Success       bool       `json:"success"`
	Attempts      int        `json:"attempts"`
	StatusCode    *int       `json:"status_code"`
	Response      string     `json:"response,omitempty"`
	Error         string     `json:"error,omitempty"`
	DurationMS    *int       `json:"duration_ms"`
	NextAttemptAt *time.Time `json:"next_attempt_at,omitempty"`
	DeliveredAt   *time.Time `json:"delivered_at,omitempty"`
	CreatedAt     time.Time  `json:"created_at"`
	UpdatedAt     time.Time  `json:"updated_at"`
}

Delivery is one event sent (or to be sent) to one webhook.

type Dispatcher added in v0.10.0

type Dispatcher struct {
	// contains filtered or unexported fields
}

Dispatcher fans events out to subscribed webhooks and delivers them.

func NewDispatcher added in v0.10.0

func NewDispatcher(repo *Repository) *Dispatcher

NewDispatcher returns a dispatcher using repo. Deliveries do not follow redirects and only reach internal addresses when AllowPrivateTargetsEnv allows it (see target.go).

func (*Dispatcher) Enqueue added in v0.10.0

func (d *Dispatcher) Enqueue(ctx context.Context, tx *sql.Tx, hooks []*Webhook, event string, occurredAt time.Time, data interface{}) (int, error)

Enqueue records a pending delivery of event for every active webhook in hooks that subscribes to it and returns how many were queued. Callers load hooks once (Repository.List) per batch of events. With a non-nil tx the rows are written inside that transaction.

func (*Dispatcher) ProcessDue added in v0.10.0

func (d *Dispatcher) ProcessDue(ctx context.Context) (int, error)

ProcessDue sends the due deliveries of every webhook, applying the retry policy of each, until none is due or ctx ends. Up to MaxParallelWebhooks webhooks are served concurrently. Deliveries still due when ctx ends stay pending for the next call; that is not an error. It returns the number of delivery attempts made.

func (*Dispatcher) Redeliver added in v0.10.0

func (d *Dispatcher) Redeliver(ctx context.Context, deliveryID int64) (*Delivery, error)

Redeliver sends the payload of an earlier delivery again as a new delivery (single attempt, no retries) and returns the new delivery.

func (*Dispatcher) Test added in v0.10.0

func (d *Dispatcher) Test(ctx context.Context, webhookID int64) (*Delivery, error)

Test sends a webhook.test event to the webhook immediately, records it as a delivery (single attempt, no retries) and returns the result.

type Envelope added in v0.10.0

type Envelope struct {
	Event      string      `json:"event"`
	OccurredAt time.Time   `json:"occurred_at"`
	Data       interface{} `json:"data"`
}

Envelope is the JSON body POSTed to webhook endpoints.

type Repository added in v0.10.0

type Repository struct {
	// contains filtered or unexported fields
}

Repository persists webhooks, deliveries and event-source cursors.

func NewRepository added in v0.10.0

func NewRepository(db *sql.DB) *Repository

NewRepository returns a repository on db.

func (*Repository) AdvanceCursor added in v0.10.0

func (r *Repository) AdvanceCursor(ctx context.Context, tx *sql.Tx, source string, from, to int64) (bool, error)

AdvanceCursor moves a cursor from "from" to "to" inside tx. It returns false when another worker already moved it, in which case the caller must roll back: the row lock taken here serialises concurrent dispatchers, so events are enqueued exactly once.

func (*Repository) Create added in v0.10.0

func (r *Repository) Create(ctx context.Context, w *Webhook, secret string, userID int) (*Webhook, error)

Create stores a new webhook (already normalised) and returns it.

func (*Repository) Cursor added in v0.10.0

func (r *Repository) Cursor(ctx context.Context, source string, start func(context.Context) (int64, error)) (int64, error)

Cursor returns the last processed id of an event source. When the source has no cursor yet it is created at start() so only later rows become events.

func (*Repository) Delete added in v0.10.0

func (r *Repository) Delete(ctx context.Context, id int64) error

Delete removes a webhook and (by cascade) its delivery log.

func (*Repository) Get added in v0.10.0

func (r *Repository) Get(ctx context.Context, id int64) (*Webhook, error)

Get returns one webhook (without header values) or ErrNotFound.

func (*Repository) GetDelivery added in v0.10.0

func (r *Repository) GetDelivery(ctx context.Context, id int64) (*Delivery, error)

GetDelivery returns one delivery including payload and response, or ErrNotFound.

func (*Repository) HeaderValues added in v0.10.0

func (r *Repository) HeaderValues(ctx context.Context, id int64) (map[string]string, error)

HeaderValues returns the decrypted custom header values of a webhook, for updates that keep some stored values.

func (*Repository) List added in v0.10.0

func (r *Repository) List(ctx context.Context, active *bool) ([]*Webhook, error)

List returns all webhooks ordered by name; active filters by state when set.

func (*Repository) ListDeliveries added in v0.10.0

func (r *Repository) ListDeliveries(ctx context.Context, webhookID int64, limit int) ([]*Delivery, error)

ListDeliveries returns the newest deliveries of a webhook without payload and response bodies (fetch a single delivery for those).

func (*Repository) PruneDeliveries added in v0.10.0

func (r *Repository) PruneDeliveries(ctx context.Context, cutoff time.Time) (int, error)

PruneDeliveries deletes delivered and failed deliveries created before cutoff and returns how many were deleted. Pending and in-flight deliveries are kept whatever their age.

func (*Repository) Update added in v0.10.0

func (r *Repository) Update(ctx context.Context, w *Webhook, secret SecretChange, userID int) (*Webhook, error)

Update stores a modified (already normalised) webhook. The custom headers are replaced when w.Headers is non-nil and kept otherwise.

type SecretChange added in v0.10.0

type SecretChange struct {
	Set   bool   // replace the secret with Value ("" removes it)
	Value string // new secret when Set
}

SecretChange tells Update what to do with the stored secret.

type ValidationError added in v0.10.0

type ValidationError struct {
	Message string
}

ValidationError describes invalid webhook configuration.

func (*ValidationError) Error added in v0.10.0

func (e *ValidationError) Error() string

type Webhook

type Webhook struct {
	ID     int64    `json:"id"`
	Name   string   `json:"name"`
	URL    string   `json:"url"`
	Events []string `json:"events"`
	// Headers holds custom header values. It is only filled when values are
	// being saved or sent, never serialised.
	Headers map[string]string `json:"-"`
	// HeaderHints maps each custom header name to a masked hint of its value.
	HeaderHints    map[string]string `json:"header_hints"`
	HasSecret      bool              `json:"has_secret"`
	SecretHint     string            `json:"secret_hint,omitempty"`
	RetryCount     int               `json:"retry_count"`
	TimeoutSeconds int               `json:"timeout_seconds"`
	IsActive       bool              `json:"is_active"`
	CreatedAt      time.Time         `json:"created_at"`
	CreatedBy      int               `json:"created_by"`
	UpdatedAt      time.Time         `json:"updated_at"`
	UpdatedBy      int               `json:"updated_by"`
}

Webhook is a configured outbound endpoint. The secret and the custom header values are write-only: they are stored encrypted and only masked hints are ever returned.

func (*Webhook) Normalize added in v0.10.0

func (w *Webhook) Normalize(knownEvent func(string) bool) error

Normalize trims input, applies defaults and validates the configuration. knownEvent reports whether an event name may be subscribed to. Headers are validated when set (nil means "not being changed").

func (*Webhook) Subscribes added in v0.10.0

func (w *Webhook) Subscribes(event string) bool

Subscribes reports whether the webhook wants deliveries for event.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL