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 ¶
- Webhook contract: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/operations/webhooks.md
- Authorization: https://github.com/deploymenttheory/go-apple-dm/blob/main/server/internal/app/webhooks.go
- Delivery implementation: https://github.com/deploymenttheory/go-apple-dm/blob/main/server/webhook/transport.go
- Cedar authorization: https://docs.cedarpolicy.com/auth/authorization.html
- RFC 2104 (HMAC): https://www.rfc-editor.org/rfc/rfc2104
- This webhook envelope is a project contract, independent of Apple's device-facing MDM and declarative management protocols.
Index ¶
- Constants
- Variables
- func BlobColumns() []sqlcommon.BlobColumn
- func ClassifyRoute(pattern, path string) (family, operation string)
- func CorrelationID(ctx context.Context) string
- func MigrationSet(d sqlcommon.Dialect) (sqlcommon.MigrationSet, error)
- func ObserveMDM(ctx context.Context, ck *mdm.Checkin, resp *mdm.Response, accepted bool)
- func ObserveOutcome(ctx context.Context, outcome string)
- func ObserveSubject(ctx context.Context, subject Subject)
- func OutcomeType(internal string) string
- func Sign(secret, id string, at time.Time, body []byte) (string, error)
- func Verify(secret string, headers http.Header, body []byte, now time.Time) error
- func WithCorrelation(ctx context.Context, id string) context.Context
- type CatalogueEntry
- type Change
- type Config
- type Credentials
- type Delivery
- type Event
- type Filters
- type Payload
- type PayloadPolicy
- type ReplayRequest
- type ReplayResult
- type Spec
- type Store
- func (s *Store) Admin(w http.ResponseWriter, r *http.Request, root bool)
- func (s *Store) Capture(ctx context.Context, e Event) error
- func (s *Store) CaptureOutcome(ctx context.Context, in event.Event) error
- func (s *Store) Create(ctx context.Context, spec Spec, root bool) (Change, error)
- func (s *Store) Deliveries(ctx context.Context, id, after string, limit int) ([]Delivery, error)
- func (s *Store) Get(ctx context.Context, id string) (Subscription, error)
- func (s *Store) List(ctx context.Context, after string, limit int) ([]Subscription, error)
- func (s *Store) Observe(next http.Handler, mux *http.ServeMux) http.Handler
- func (s *Store) ObserveCertificates(repository state.Store) state.Store
- func (s *Store) PayloadHandler() http.Handler
- func (s *Store) Prune(ctx context.Context) error
- func (s *Store) Replay(ctx context.Context, req ReplayRequest, root bool) (ReplayResult, error)
- func (s *Store) Resolve(_ context.Context, dest string) (eventstore.Sender, error)
- func (s *Store) Retry(ctx context.Context, id string, root bool) error
- func (s *Store) Rewrap(ctx context.Context) (int, error)
- func (s *Store) Rotate(ctx context.Context, id string, overlap time.Duration, root bool) (Change, error)
- func (s *Store) RunRetention(ctx context.Context) error
- func (s *Store) Send(ctx context.Context, id string) error
- func (s *Store) SetState(ctx context.Context, id, action string, root bool) (Subscription, error)
- func (s *Store) Status(ctx context.Context) (map[string]any, error)
- func (s *Store) Test(ctx context.Context, id string, root bool) (string, error)
- func (s *Store) Update(ctx context.Context, id string, revision int, spec Spec, root bool) (Change, error)
- type Subject
- type Subscription
Constants ¶
const ( HeaderID = "webhook-id" HeaderTimestamp = "webhook-timestamp" HeaderSignature = "webhook-signature" )
const ( SchemaVersion = "1" InlineLimit = 256 << 10 DefaultMaxBody = 16 << 20 DefaultPayloadRetention = 7 * 24 * time.Hour DefaultMetadataRetention = 30 * 24 * time.Hour PayloadPath = "/webhooks/v1/payloads/" )
Variables ¶
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 ¶
ClassifyRoute deliberately excludes administration, internal DDM forwarding, health endpoints and payload retrieval. A missing route produces no event.
func CorrelationID ¶
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 ¶
ObserveMDM records decoded vocabulary and the independently determined service result. HTTP 200 alone is never interpreted as successful MDM authorization.
func ObserveOutcome ¶
ObserveOutcome records the service result independently of its HTTP status.
func ObserveSubject ¶
ObserveSubject records a resource only after the server has authenticated it.
func OutcomeType ¶
OutcomeType maps a reviewed internal event name to the native server event vocabulary. Unknown internal events return an empty string and are not captured.
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).
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Get returns subscription configuration and state without signing or payload credentials. Missing subscriptions return ErrNotFound.
func (*Store) List ¶
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 ¶
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 ¶
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 ¶
PayloadHandler authenticates only subscription-scoped receiver credentials. It accepts no administrator tokens and never grants administrative access.
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) Retry ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Status returns delivery counts and oldest occurrence times by state, capture failures, and configured retention periods without exposing destination secrets or captured payloads.
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.