webhook

package
v0.11.0-cloud1 Latest Latest
Warning

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

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

Documentation

Overview

Package webhook implements the Platform API control-plane webhook receiver.

It is the receiver-side counterpart to the Developer Portal producer described in docs-local/devportal-webhook-integration.md. The Developer Portal commits a domain change, writes it to a transactional outbox, and delivers signed webhooks (at-least-once) to this endpoint. Platform API verifies the request, decrypts any sensitive fields, reconciles its own state by reusing the existing API-key / subscription services, and lets the existing EventHub -> WebSocket sync propagate the change to gateway replicas.

Index

Constants

View Source
const (
	EventAPIKeyGenerated          = "apikey.generated"
	EventAPIKeyRegenerated        = "apikey.regenerated"
	EventAPIKeyRevoked            = "apikey.revoked"
	EventAPIKeyApplicationUpdated = "apikey.application_updated"
)

API key event types.

View Source
const (
	EventApplicationCreated = "application.created"
	EventApplicationUpdated = "application.updated"
	EventApplicationDeleted = "application.deleted"
)

Application event types. apikey.application_updated is intentionally handled with the other apikey.* events (see handlers_apikey.go): it targets an API key, not an application.

View Source
const (
	EventSubscriptionCreated          = "subscription.created"
	EventSubscriptionUpdated          = "subscription.updated"           // status change (ACTIVE ↔ INACTIVE)
	EventSubscriptionPlanChanged      = "subscription.plan_changed"      // plan switched; token unchanged
	EventSubscriptionTokenRegenerated = "subscription.token_regenerated" // token rotated (encrypted)
	EventSubscriptionDeleted          = "subscription.deleted"
)

Subscription event types.

View Source
const CurrentSchemaVersion = "1.0"

CurrentSchemaVersion is assumed when an event omits schema_version. The producer lists schema_version as forward-compat work, so a missing value must be tolerated (not rejected).

View Source
const RoutePath = "/api/internal/" + constants.APIVersion + "/webhook/events"

RoutePath is the webhook endpoint under the resource API version prefix (constants.APIVersion, currently "v0.9"). It lives under the internal/control-plane prefix and is excluded from JWT/IDP auth via config.Auth.SkipPaths (authenticity comes from the HMAC signature).

Variables

View Source
var (
	// ErrSignatureMissing indicates the signature header was absent. -> 401
	ErrSignatureMissing = errors.New("webhook signature header missing")
	// ErrSignatureInvalid indicates the HMAC did not match. -> 401
	ErrSignatureInvalid = errors.New("webhook signature invalid")
	// ErrTimestampOutOfTolerance indicates a possible replay. -> 401
	ErrTimestampOutOfTolerance = errors.New("webhook signature timestamp outside tolerance")

	// ErrInvalidEnvelope indicates a malformed body or missing required field. -> 400
	ErrInvalidEnvelope = errors.New("invalid webhook envelope")
	// ErrUnsupportedEvent indicates an unknown event_type. -> 400
	ErrUnsupportedEvent = errors.New("unsupported webhook event type")
	// ErrBodyTooLarge indicates the body exceeded max_body_size. -> 400
	ErrBodyTooLarge = errors.New("webhook request body too large")

	// ErrDecryptionFailed indicates the encrypted field could not be decrypted. -> 400
	ErrDecryptionFailed = errors.New("failed to decrypt webhook payload field")
	// ErrDecryptorUnavailable indicates an encrypted field was received but no private key is configured. -> 500
	ErrDecryptorUnavailable = errors.New("webhook decryptor is not configured")
)

Functions

This section is empty.

Types

type Decryptor

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

Decryptor recovers a plaintext secret from the hybrid-encrypted EncryptedKey field.

The producer (Developer Portal) encrypts the secret with a one-time AES-256 content key, then wraps that content key with this receiver's RSA public key (RSA-OAEP). Decryption is therefore two stages:

  1. RSA-OAEP unwrap `wrappedKey` with the configured RSA private key -> AES content key.
  2. AES-256-GCM decrypt `ciphertext` (with `iv` as nonce and `tag` appended) -> plaintext.

Interop note: this assumes RSA-OAEP with SHA-256 and no OAEP label, a 12-byte GCM nonce, and a separate 16-byte GCM tag. These must match the producer; they are documented in docs-local/platform-api-webhook.md as parameters to confirm with the Developer Portal team.

func NewDecryptor

func NewDecryptor(pemPath string) (*Decryptor, error)

NewDecryptor loads a PEM-encoded RSA private key (PKCS#1 or PKCS#8) from pemPath. A nil Decryptor is valid and means "no key configured"; Decrypt then returns ErrDecryptorUnavailable so events carrying encrypted fields fail loudly rather than silently.

func (*Decryptor) Decrypt

func (d *Decryptor) Decrypt(ek *EncryptedKey) (string, error)

Decrypt returns the plaintext secret for the given encrypted field. The caller must clear the returned plaintext from memory as soon as the gateway-side representation (e.g. a hash) is derived.

type EncryptedKey

type EncryptedKey struct {
	WrappedKey string `json:"wrappedKey"`
	IV         string `json:"iv"`
	Tag        string `json:"tag"`
	Ciphertext string `json:"ciphertext"`
}

EncryptedKey is the hybrid-encrypted (AES-256-GCM + RSA-OAEP) representation of a sensitive value (an API key secret, etc.). wrappedKey is the AES content key wrapped with the receiver's RSA public key; iv/tag/ciphertext are the AES-GCM parts. See Decryptor.

func (*EncryptedKey) Empty

func (e *EncryptedKey) Empty() bool

Empty reports whether the encrypted field is unset.

type Envelope

type Envelope struct {
	EventID    string `json:"event_id"`
	EventType  string `json:"event_type"`
	OccurredAt string `json:"occurred_at"`
	Org        orgRef `json:"org"`
	// OrgID is the legacy flat org identifier, superseded by Org.RefID. DecodeEnvelope copies
	// Org.RefID into it when present, so downstream handlers can keep reading env.OrgID.
	OrgID         string `json:"org_id"`
	GatewayType   string `json:"gateway_type"`
	AggregateType string `json:"aggregate_type"`
	AggregateID   string `json:"aggregate_id"`
	SchemaVersion string `json:"schema_version"`
	// EncryptedFields lists which fields inside Data carry an encrypted envelope. Always present
	// in the new contract — empty when no fields are encrypted.
	EncryptedFields []string        `json:"encrypted_fields"`
	Data            json.RawMessage `json:"data"`
}

Envelope is the common webhook event envelope. It mirrors the Developer Portal DP_EVENT outbox row. Data carries the event-type-specific payload and is decoded by each handler.

func DecodeEnvelope

func DecodeEnvelope(body []byte) (*Envelope, error)

DecodeEnvelope parses the raw request body into an Envelope. A decode failure maps to 400.

func (*Envelope) IsEncrypted

func (e *Envelope) IsEncrypted(field string) bool

IsEncrypted reports whether the named Data field carries an encrypted envelope, per the envelope's encrypted_fields list.

func (*Envelope) Validate

func (e *Envelope) Validate() error

Validate checks that the mandatory envelope fields are present. schema_version is optional; when absent it is treated as CurrentSchemaVersion rather than rejected.

type Receiver

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

Receiver is the gin handler implementing the webhook processing flow.

func NewReceiver

func NewReceiver(
	cfg config.Webhook,
	decryptor *Decryptor,
	apiKeys apiKeyService,
	subs subscriptionService,
	apps applicationService,
	orgs organizationResolver,
	slogger *slog.Logger,
) *Receiver

NewReceiver wires the receiver and its event-type dispatch table.

func (*Receiver) ReceiveEvent

func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request)

ReceiveEvent runs the full webhook flow: size-limited read -> signature verify -> envelope decode/validate -> gateway_type filter -> idempotency -> dispatch -> mark processed.

func (*Receiver) RegisterRoutes

func (r *Receiver) RegisterRoutes(mux *http.ServeMux)

RegisterRoutes registers the webhook endpoint on the mux. Only called when the webhook is enabled.

type Verifier

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

Verifier validates the HMAC-SHA256 signature of an incoming webhook request.

The Developer Portal signs each request with a per-subscriber shared secret. The signature header has the form "t=<unix-seconds>,v1=<hex-hmac>" and the signed payload is "<timestamp>.<raw_body>". This matches the producer scheme documented in docs-local/platform-api-webhook.md.

func NewVerifier

func NewVerifier(secret string, tolerance time.Duration) *Verifier

NewVerifier builds a Verifier. tolerance bounds how old a signed request may be (replay guard).

func (*Verifier) Verify

func (v *Verifier) Verify(header string, body []byte, now time.Time) error

Verify checks the signature header against the raw request body. It returns nil when the request is authentic and a sentinel error (ErrSignature*/ErrTimestampOutOfTolerance) otherwise. now is injected for testability; callers pass time.Now().

Jump to

Keyboard shortcuts

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