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 ¶
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 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:
- RSA-OAEP unwrap `wrappedKey` with the configured RSA private key -> AES content key.
- 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 ¶
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.
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 ¶
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, 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 ¶
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 ¶
NewVerifier builds a Verifier. tolerance bounds how old a signed request may be (replay guard).