webhook

package
v0.9.1 Latest Latest
Warning

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

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

Documentation

Overview

Package webhook sends one event to one subscriber (automation.md §3.1).

It is an outbound adapter and nothing else: what to send and to whom was decided by the fan-out, and what a failure means is decided by the aggregate. What lives here is the part that needs the network - the body, the signature, the call through the guard, and the translation of an HTTP answer into "try again" or "stop".

Index

Constants

View Source
const (
	EventIDHeader   = "X-Hubtask-Event-Id"
	EventTypeHeader = "X-Hubtask-Event-Type"
	AttemptHeader   = "X-Hubtask-Delivery-Attempt"
)

The headers automation.md §3.1 names, beside the signature.

Variables

This section is empty.

Functions

This section is empty.

Types

type Deliverer

type Deliverer struct {
	Subscriptions repository.WebhookSubscriptions
	Deliveries    repository.WebhookDeliveries
	Events        Events
	Outcomes      Outcomes
	// Encryptor opens the sealed signing secret. This is the only place a webhook secret is ever
	// in memory in clear, and it is in memory for the length of one HMAC.
	Encryptor crypto.Encryptor
	Signer    security.WebhookSigner
	// Client is the guarded one, always. A webhook target is an egress channel exactly as a backup
	// target is: a private range or the cloud metadata address is refused here unless the
	// installation has deliberately released private networks (rule 6, T-07).
	Client httpport.Port
	// UnitOfWork is this handler's own, because it is Detached: the call to somebody else's server
	// happens between two short transactions rather than inside one long one. Holding a database
	// connection for as long as a subscriber's server feels like taking is exactly what
	// observability-reliability.md §8 forbids.
	UnitOfWork persistence.UnitOfWork
	// Jobs is where the next attempt is asked for. A job per attempt rather than a repeating job,
	// so that each attempt has its own row and the log can answer "this event was tried eight
	// times over two days and these are the answers".
	Jobs  Queue
	Clock clock.Clock
	IDs   clock.IDGenerator
	// Source is what the CloudEvent names as its origin. The installation's own identifier, so a
	// subscriber receiving from two installations can tell them apart.
	Source string
	// NextAttempt is the backoff, given the attempts made so far. Injected for the runner's
	// reason: this layer decides when to retry, not how far apart.
	NextAttempt func(attempt int) time.Duration
	// Entitlements is what the workspace's plan leaves out (multi-tenancy.md §4.2). Nil includes
	// every feature.
	Entitlements adminrepo.Entitlements
	// Signals carries each settled attempt into the metrics. Nil skips; the composition root
	// always wires it.
	Signals Signals
}

Deliverer sends one delivery. It is a queue handler, so the retry ladder is the queue's and the backoff policy is the resilience adapter's - eight attempts reaching a day is a schedule this system already knows how to keep.

func (Deliverer) OwnsItsTransactions

func (Deliverer) OwnsItsTransactions()

OwnsItsTransactions declares the exception this handler needs (queue.Detached).

What it gives up is the atomicity everybody else gets for free, and that is acceptable here for the reason the interface asks about: a delivery is safe to run twice. At-least-once is the contract ADR-0007 states, a subscriber deduplicates on X-Hubtask-Event-Id, and a repeat that finds its attempt row already settled stops without sending anything.

func (Deliverer) Run

func (d Deliverer) Run(ctx context.Context, job queue.Job) (queue.Result, error)

Run makes one attempt.

It returns no error for a target that refused, and that is the shape the whole discipline rests on: a failed delivery is this system working correctly, and a job that failed would be retried by the queue on the queue's schedule rather than on the delivery's, and would eventually reach the queue's dead letter rather than the delivery log an operator reads. The retry is asked for explicitly instead.

type Events

type Events interface {
	FindEvent(ctx context.Context, id shared.ID) (event.Envelope, error)
}

Events is the slice of the outbox this adapter reads: one event, by its identifier.

A delivery renders the event as it was rather than carrying a copy in the job payload, so a retry two days later sends what the first attempt would have - and a job row does not become a second place a workspace's content lives.

type Outcomes

type Outcomes interface {
	Delivered(ctx context.Context, subscriptionID shared.ID) error
	Failed(ctx context.Context, subscriptionID shared.ID, code string, terminal bool) error
}

Outcomes is what an attempt reports back, and what decides the failure run and the auto-disable.

Declared here rather than imported, because an outbound adapter may not call a use case (project-structure.md §2, and the gate that says so). The composition root puts the two together; what this package knows is that something answers these two questions.

type PlatformDeliverer

type PlatformDeliverer struct {
	Subscriptions adminrepo.PlatformSubscriptions
	Deliveries    adminrepo.PlatformDeliveries
	Events        PlatformEvents
	// Encryptor opens the sealed secret for the length of one HMAC.
	Encryptor crypto.Encryptor
	Signer    security.WebhookSigner
	// Client is the guarded one (rule 6): a private range or the metadata address is refused
	// unless the installation released private networks.
	Client httpport.Port
	// UnitOfWork is this handler's own: the call to the platform happens between two short
	// transactions, never inside one.
	UnitOfWork  persistence.UnitOfWork
	Jobs        Queue
	Clock       clock.Clock
	Source      string
	NextAttempt func(attempt int) time.Duration
	Signals     PlatformSignals
}

PlatformDeliverer sends one installation event to one platform subscription (UC-INS-14): the workspace webhook's signature, headers and retry ladder, and none of its switching off. A delivery that ends without arriving stays as a dead letter the operator redelivers, and the subscription stays as it was - the operator is told by A-21, not by the subscription going quiet.

func (PlatformDeliverer) OwnsItsTransactions

func (PlatformDeliverer) OwnsItsTransactions()

OwnsItsTransactions declares the exception: a delivery is safe to run twice, since a platform deduplicates on X-Hubtask-Event-Id and an attempt is recorded only against the attempt it read.

func (PlatformDeliverer) Run

Run makes one attempt. A target that refused is no error: the retry is asked for explicitly, on the delivery's ladder rather than the queue's.

type PlatformEvents

type PlatformEvents interface {
	FindEvent(ctx context.Context, id shared.ID) (event.Envelope, error)
}

PlatformEvents is the slice of the installation's events a delivery reads: one, by its id.

type PlatformSignals

type PlatformSignals interface {
	PlatformDelivery(ctx context.Context, result, statusClass string)
}

PlatformSignals carries each settled attempt into the metrics, as the workspace's deliveries do.

type Queue

type Queue interface {
	Enqueue(ctx context.Context, request queue.Request) (shared.ID, error)
}

Queue is where the next attempt is asked for. The same one line the fan-out needs, declared again for the same reason.

type Signals

type Signals interface {
	WebhookDelivery(ctx context.Context, result, statusClass string)
}

Signals is the delivery's trace in the metrics (observability-reliability.md §4): every settled attempt, by what became of it - ok, retry, dead - and the class of the answer. SLO-6's error rate excludes 4xx recipient errors, which is exactly what the class label makes possible. A local interface rather than the metrics adapter, because adapters do not know each other.

Jump to

Keyboard shortcuts

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