webhook

package
v0.10.7 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Overview

Package webhook implements the reference server's native webhook contract, subscription management, encrypted capture and receiver authentication.

Delivery uses eventstore's persistent leases. Payloads are immutable for a delivery; replay creates another delivery for the same occurrence. Network delivery is at least once and unordered. Consumers deduplicate delivery IDs. Full decoded bodies and original bytes require explicit sensitive-webhook authority supplied by the host. The reference server evaluates Cedar actions for sensitive subscription management and replay separately from ordinary route authorization. The boolean named root in the API carries this decision; it does not authenticate a principal or evaluate policies itself.

Managed delivery requires SQL persistence and an encryption keyring. Signing secrets are returned only on creation or rotation; payload tokens authorize receiver retrieval of retained payloads. Retention and replay preserve the disclosure ceiling of the selected captured representation.

References

Index

Constants

View Source
const (
	HeaderID        = "webhook-id"
	HeaderTimestamp = "webhook-timestamp"
	HeaderSignature = "webhook-signature"
)
View Source
const (
	SchemaVersion            = "1"
	InlineLimit              = 256 << 10
	DefaultMaxBody           = 16 << 20
	DefaultPayloadRetention  = 7 * 24 * time.Hour
	DefaultMetadataRetention = 30 * 24 * time.Hour
	PayloadPath              = "/webhooks/v1/payloads/"
)

Variables

View Source
var (
	ErrInvalid   = errors.New("webhook: invalid argument")
	ErrNotFound  = errors.New("webhook: not found")
	ErrForbidden = errors.New("webhook: root or receiver authority required")
	ErrConflict  = errors.New("webhook: conflicting revision or delivery state")
	ErrExpired   = errors.New("webhook: payload expired")
)

Functions

func BlobColumns

func BlobColumns() []sqlcommon.BlobColumn

BlobColumns registers every sealed column for backup verification and rekey.

func ClassifyRoute

func ClassifyRoute(pattern, path string) (family, operation string)

ClassifyRoute deliberately excludes administration, internal DDM forwarding, health endpoints and payload retrieval. A missing route produces no event.

func CorrelationID

func CorrelationID(ctx context.Context) string

CorrelationID returns the exchange correlation identifier stored in ctx, or an empty string when no observation has assigned one.

func MigrationSet

func MigrationSet(d sqlcommon.Dialect) (sqlcommon.MigrationSet, error)

MigrationSet returns the embedded webhook migrations for SQLite, PostgreSQL, or MySQL. Other dialects return ErrInvalid.

func ObserveMDM

func ObserveMDM(ctx context.Context, ck *mdm.Checkin, resp *mdm.Response, accepted bool)

ObserveMDM records decoded vocabulary and the independently determined service result. HTTP 200 alone is never interpreted as successful MDM authorization.

func ObserveOutcome

func ObserveOutcome(ctx context.Context, outcome string)

ObserveOutcome records the service result independently of its HTTP status.

func ObserveSubject

func ObserveSubject(ctx context.Context, subject Subject)

ObserveSubject records a resource only after the server has authenticated it.

func OutcomeType

func OutcomeType(internal string) string

OutcomeType maps a reviewed internal event name to the native server event vocabulary. Unknown internal events return an empty string and are not captured.

func Sign

func Sign(secret, id string, at time.Time, body []byte) (string, error)

Sign follows Standard Webhooks: id.timestamp.body with a v1 HMAC-SHA256.

func Verify

func Verify(secret string, headers http.Header, body []byte, now time.Time) error

Verify authenticates the exact request bytes within a five-minute freshness window. Receivers must also persist delivery IDs to deduplicate retries.

func WithCorrelation

func WithCorrelation(ctx context.Context, id string) context.Context

WithCorrelation is for authenticated internal transports. Public ingress always replaces caller-provided identifiers before processing a request.

Types

type CatalogueEntry

type CatalogueEntry struct {
	Type          string   `json:"type"`
	Operations    []string `json:"operations,omitempty"`
	SummaryFields []string `json:"summary_fields"`
	FullJSON      bool     `json:"full_json"`
	Raw           bool     `json:"raw"`
}

CatalogueEntry describes one native webhook event type and the payload representations available for capture.

func Catalogue

func Catalogue() []CatalogueEntry

Catalogue returns a fresh copy of the native vocabulary.

type Change

type Change struct {
	Subscription Subscription `json:"subscription"`
	Credentials  *Credentials `json:"credentials,omitempty"`
}

Change returns a subscription mutation and, only when created or rotated, the newly issued receiver credentials.

type Config

type Config struct {
	Enabled           bool
	Source            string
	PayloadRetention  time.Duration
	MetadataRetention time.Duration
	MaxBody           int
	PrivateNetworks   []string
	RootCAFile        string
	Client            *http.Client
	Now               func() time.Time
	// CommandType looks up already-authorized command metadata in the caller's
	// transaction. Missing/cleared commands return an empty type.
	CommandType func(context.Context, mdm.EnrollmentID, string) (string, error)
}

Config controls capture bounds and transport. PrivateNetworks explicitly opts an operator's trusted receiver networks into the outbound dial policy.

type Credentials

type Credentials struct {
	SigningSecret string `json:"signing_secret"`
	PayloadToken  string `json:"payload_token"`
}

Credentials are returned only when created or rotated; ordinary reads omit them.

type Delivery

type Delivery struct {
	ID             string    `json:"id"`
	EventID        string    `json:"event_id"`
	SubscriptionID string    `json:"subscription_id"`
	Revision       int       `json:"revision"`
	State          string    `json:"state"`
	Attempts       int       `json:"attempts"`
	NextAttempt    time.Time `json:"next_attempt"`
	LastCode       string    `json:"last_code"`
	ExpiresAt      time.Time `json:"expires_at"`
	Sensitive      bool      `json:"sensitive"`
	Event          Event     `json:"event"`
}

Delivery is the administrative view of a delivery attempt sequence, retaining its occurrence ID, subscription revision, and delivery state without captured bodies.

type Event

type Event struct {
	SchemaVersion string             `json:"schema_version"`
	EventID       string             `json:"event_id"`
	Type          string             `json:"type"`
	OccurredAt    time.Time          `json:"occurred_at"`
	Source        string             `json:"source"`
	Subject       *Subject           `json:"subject,omitempty"`
	CorrelationID string             `json:"correlation_id,omitempty"`
	Data          map[string]any     `json:"data"`
	Payloads      map[string]Payload `json:"payloads,omitempty"`
}

Event is the immutable occurrence carried by one or more deliveries.

type Filters

type Filters struct {
	Operations   []string `json:"operations,omitempty"`
	Outcomes     []string `json:"outcomes,omitempty"`
	Channels     []string `json:"channels,omitempty"`
	Subjects     []string `json:"subjects,omitempty"`
	CommandTypes []string `json:"command_types,omitempty"`
}

Filters use OR within a field and AND between fields. Subject filters never match unverified claims or exchanges without a verified subject.

type Payload

type Payload struct {
	ContentType  string          `json:"content_type"`
	Encoding     string          `json:"encoding"`
	Availability string          `json:"availability"`
	Size         int             `json:"size"`
	SHA256       string          `json:"sha256,omitempty"`
	Value        json.RawMessage `json:"value,omitempty"`
	Href         string          `json:"href,omitempty"`
	ExpiresAt    *time.Time      `json:"expires_at,omitempty"`
}

Payload is either inline, referenced, or explicitly unavailable. SHA256 and Size describe the representation fetched from Href (raw bytes or JSON).

func BodyPayload

func BodyPayload(body []byte, contentType string, asJSON bool) Payload

BodyPayload preserves bytes without a lossy intermediate number conversion.

type PayloadPolicy

type PayloadPolicy struct {
	FullJSON    bool `json:"full_json"`
	RawRequest  bool `json:"raw_request"`
	RawResponse bool `json:"raw_response"`
}

PayloadPolicy is a disclosure ceiling, including when replaying older data.

func (PayloadPolicy) Sensitive

func (p PayloadPolicy) Sensitive() bool

Sensitive reports whether the policy permits complete decoded bodies or original request or response bytes.

type ReplayRequest

type ReplayRequest struct {
	SubscriptionID string    `json:"subscription_id"`
	EventIDs       []string  `json:"event_ids,omitempty"`
	Type           string    `json:"type,omitempty"`
	After          time.Time `json:"after,omitempty"`
	Before         time.Time `json:"before,omitempty"`
	Key            string    `json:"key"`
	DryRun         bool      `json:"dry_run"`
	Limit          int       `json:"limit,omitempty"`
}

ReplayRequest selects retained occurrences for a destination. Key makes a non-dry replay idempotent; DryRun reports the selection without creating deliveries.

type ReplayResult

type ReplayResult struct {
	EventIDs    []string            `json:"event_ids"`
	DeliveryIDs []string            `json:"delivery_ids"`
	DryRun      bool                `json:"dry_run"`
	Truncated   bool                `json:"truncated"`
	Missing     map[string][]string `json:"missing_payloads,omitempty"`
}

ReplayResult reports selected occurrence IDs, newly created delivery IDs, missing representations, and whether the bounded selection was truncated.

type Spec

type Spec struct {
	Name    string        `json:"name"`
	URL     string        `json:"url"`
	Events  []string      `json:"events"`
	Filters Filters       `json:"filters"`
	Payload PayloadPolicy `json:"payload"`
}

Spec is the writable subscription configuration. URLs may contain secrets and are only exposed to webhook administrators, never through delivery diagnostics.

type Store

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

Store persists encrypted webhook subscriptions, captured payloads, and delivery metadata.

func Open

func Open(ctx context.Context, db *sql.DB, d sqlcommon.Dialect, keys *crypt.Keyring, outbox *eventstore.Store, cfg Config) (*Store, error)

Open requires encryption even for summary-only subscriptions because signing keys are recoverable secrets. It never owns the supplied SQL pool.

func (*Store) Admin

func (s *Store) Admin(w http.ResponseWriter, r *http.Request, root bool)

Admin serves after the host application has authorized the ordinary route. The root argument carries a separate sensitive-operation authorization decision; the reference server derives it from the relevant Cedar action. Admin does not authenticate callers or evaluate policies.

func (*Store) Capture

func (s *Store) Capture(ctx context.Context, e Event) error

Capture participates in the caller's SQL transaction. It copies payloads at capture time, never retains mutable pointers, and records nothing when no enabled subscription matches.

func (*Store) CaptureOutcome

func (s *Store) CaptureOutcome(ctx context.Context, in event.Event) error

CaptureOutcome adapts existing in-process events without changing library APIs. The projection remains the summary; complete internal data is a distinct, separately authorized sensitive representation.

func (*Store) Create

func (s *Store) Create(ctx context.Context, spec Spec, root bool) (Change, error)

Create persists an enabled subscription and returns its credentials once. Sensitive payload policies require the caller-supplied authority flag; the host must authorize ordinary subscription management separately.

func (*Store) Deliveries

func (s *Store) Deliveries(ctx context.Context, id, after string, limit int) ([]Delivery, error)

Deliveries lists delivery metadata after an exclusive delivery-ID cursor. A nonempty id selects a subscription or exact delivery; a zero limit selects 100 and the maximum is 1000. Captured payload bodies are not returned.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id string) (Subscription, error)

Get returns subscription configuration and state without signing or payload credentials. Missing subscriptions return ErrNotFound.

func (*Store) List

func (s *Store) List(ctx context.Context, after string, limit int) ([]Subscription, error)

List returns subscription metadata after an exclusive ID cursor, without credentials. A zero limit selects 100; limits outside 1 through 1000 return ErrInvalid.

func (*Store) Observe

func (s *Store) Observe(next http.Handler, mux *http.ServeMux) http.Handler

Observe captures only bytes consumed/written by the handler. It never drains rejected bodies or changes protocol responses when observation persistence fails. Capture failures remain visible through Status.

func (*Store) ObserveCertificates

func (s *Store) ObserveCertificates(repository state.Store) state.Store

ObserveCertificates joins SQL-backed managed certificate transitions and capture in one transaction. The supplied store must use this Store's SQL pool. The concrete state.Tx reaches the callback unchanged, preserving publication adapters; private material is never extracted from state records.

func (*Store) PayloadHandler

func (s *Store) PayloadHandler() http.Handler

PayloadHandler authenticates only subscription-scoped receiver credentials. It accepts no administrator tokens and never grants administrative access.

func (*Store) Prune

func (s *Store) Prune(ctx context.Context) error

Prune expires payloads before deleting metadata. Replay never changes expiry.

func (*Store) Replay

func (s *Store) Replay(ctx context.Context, req ReplayRequest, root bool) (ReplayResult, error)

Replay selects retained occurrences and creates fresh deliveries under the destination's current disclosure policy. It never reconstructs missing payloads from live state. Non-dry runs require an idempotency key; reusing it with different input returns ErrConflict. The authority flag controls access to sensitive captures.

func (*Store) Resolve

func (s *Store) Resolve(_ context.Context, dest string) (eventstore.Sender, error)

Resolve integrates native deliveries with the existing persistent worker.

func (*Store) Retry

func (s *Store) Retry(ctx context.Context, id string, root bool) error

Retry requeues the same retained delivery for its original subscription revision. Expired payloads or changed, paused, disabled, or deleted destinations cannot be retried; sensitive captures require the authority flag.

func (*Store) Rewrap

func (s *Store) Rewrap(ctx context.Context) (int, error)

Rewrap reseals encrypted webhook columns under the active storage key and returns the number rewritten. Previously accepted keys must remain available during rotation.

func (*Store) Rotate

func (s *Store) Rotate(ctx context.Context, id string, overlap time.Duration, root bool) (Change, error)

Rotate replaces signing and payload credentials and returns the new secrets once. The old signing key remains usable for the requested overlap, from zero to seven days; the old payload token is replaced immediately. Sensitive subscriptions require the authority flag.

func (*Store) RunRetention

func (s *Store) RunRetention(ctx context.Context) error

RunRetention prunes expired payloads and metadata immediately and then once per minute until cancellation. A pruning error stops the worker so the host can expose the failure.

func (*Store) Send

func (s *Store) Send(ctx context.Context, id string) error

Send attempts one signed delivery of the retained immutable body. It checks the current subscription state, includes overlapping signing keys when configured, and reports receiver status and Retry-After to the worker. It neither acquires a delivery lease nor schedules retries itself.

func (*Store) SetState

func (s *Store) SetState(ctx context.Context, id, action string, root bool) (Subscription, error)

SetState pauses, resumes, disables, enables, or deletes a subscription and updates its queued delivery state atomically. Sensitive subscriptions require the caller-supplied authority flag; deleted subscriptions cannot be changed.

func (*Store) Status

func (s *Store) Status(ctx context.Context) (map[string]any, error)

Status returns delivery counts and oldest occurrence times by state, capture failures, and configured retention periods without exposing destination secrets or captured payloads.

func (*Store) Test

func (s *Store) Test(ctx context.Context, id string, root bool) (string, error)

Test queues a synthetic occurrence directly to the selected subscription.

func (*Store) Update

func (s *Store) Update(ctx context.Context, id string, revision int, spec Spec, root bool) (Change, error)

Update uses an expected revision to prevent lost operator updates. Old deliveries remain paused; only explicit replay can move them to the new URL.

type Subject

type Subject struct {
	Kind     string `json:"kind"`
	ID       string `json:"id"`
	Channel  string `json:"channel,omitempty"`
	ParentID string `json:"parent_id,omitempty"`
}

Subject identifies a verified resource. Unverified device claims belong in Data.

type Subscription

type Subscription struct {
	ID       string `json:"id"`
	Revision int    `json:"revision"`
	Spec
	Enabled   bool      `json:"enabled"`
	Paused    bool      `json:"paused"`
	Deleted   bool      `json:"deleted"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Subscription is the persisted public configuration and lifecycle state of a webhook destination; it excludes receiver credentials.

Jump to

Keyboard shortcuts

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