Documentation
¶
Overview ¶
Package webhook implements the Platform API control-plane webhook receiver.
It is the receiver-side counterpart to the API Portal producer described in docs-local/devportal-webhook-integration.md. The API 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 ¶
const ( EventAPIKeyGenerated = "apikey.generated" EventAPIKeyRegenerated = "apikey.regenerated" EventAPIKeyRevoked = "apikey.revoked" EventAPIKeyApplicationUpdated = "apikey.application_updated" )
API key event types.
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.
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.
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).
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 ¶
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 = 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 an encrypted EncryptedKey field.
The producer (API Portal) and this receiver share one per-subscriber secret, which serves two purposes: HMAC request signing (see Verifier) and field encryption. Rather than using that secret's bytes directly for both, each side derives a separate AES-256 key from it with HKDF-SHA3-256 under fieldKeyInfo, so the encryption key is domain-separated from the signing key. Decryption is therefore a single stage: AES-256-GCM open with the derived key.
Interop note: this assumes HKDF-SHA3-256 with an empty salt (RFC 5869 falls back to HashLen zero bytes), a 12-byte GCM nonce in `iv`, and a separate 16-byte GCM tag in `tag`. These must match the producer.
func NewDecryptor ¶
NewDecryptor derives the field-encryption key from the shared webhook secret. A nil Decryptor is valid and means "no secret configured"; Decrypt then returns ErrDecryptorUnavailable so events carrying encrypted fields fail loudly rather than silently.
type EncryptedKey ¶
type EncryptedKey struct {
IV string `json:"iv"`
Tag string `json:"tag"`
Ciphertext string `json:"ciphertext"`
}
EncryptedKey is the AES-256-GCM encrypted representation of a sensitive value (an API key secret, etc.). The key is derived from the shared webhook secret via HKDF-SHA3-256, so no asymmetric key material is involved; 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"`
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 API Portal DP_EVENT outbox row. Data carries the event-type-specific payload and is decoded by each handler.
func DecodeEnvelope ¶
DecodeEnvelope parses the raw request body into an Envelope. A decode failure maps to 400.
func (*Envelope) IsEncrypted ¶
IsEncrypted reports whether the named Data field carries an encrypted envelope, per the envelope's encrypted_fields list.
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, apiKeys apiKeyService, subs subscriptionService, apps applicationService, orgs organizationResolver, slogger *slog.Logger, ) (*Receiver, error)
NewReceiver wires the receiver and its event-type dispatch table.
The verifier and the decryptor are both built from cfg.Secret here rather than passed in separately: the producer signs with that secret and derives its field-encryption key from the same secret, so keying the two off one config value is what keeps them from drifting apart.
func (*Receiver) ReceiveEvent ¶
ReceiveEvent runs the full webhook flow: size-limited read -> signature verify -> envelope decode/validate -> idempotency -> dispatch -> mark processed.
func (*Receiver) RegisterRoutes ¶
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 API 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 ¶
NewVerifier builds a Verifier. tolerance bounds how old a signed request may be (replay guard).