Generic webhook connector
GenericWebhookConnector is the built-in raw HTTP ingress primitive for
generic webhook deliveries. It verifies supported HMAC signature conventions
against the raw request body, normalizes the delivery into a TriggerEvent
with the built-in GenericWebhookPayload shape, and relies on the
orchestrator listener for route selection, backpressure, and inbox dedupe.
This connector is intentionally provider-neutral:
- route-backed ingestion through
harn orchestrator serve - raw-body verification for Standard Webhooks, Stripe-style, and GitHub-style signatures
TriggerEventnormalization with header redaction,raw_bodyretention, and provider payload preservation- durable inbox-backed dedupe keyed by the normalized
event.dedupe_keywhen the trigger manifest opts intodedupe_key
Manifest shape
[[triggers]]
id = "incoming-webhook"
kind = "webhook"
provider = "webhook"
match = { path = "/hooks/incoming" }
handler = "handlers::on_webhook"
dedupe_key = "event.dedupe_key"
secrets = { signing_secret = "webhook/incoming" }
[triggers.webhook]
signature_scheme = "standard" # "standard" | "stripe" | "github"
timestamp_tolerance_secs = 300
source = "incoming"
signature_scheme defaults to "standard" when omitted. Standard Webhooks and
Stripe-style signatures default to a 5-minute timestamp tolerance. GitHub-style
signatures are untimestamped and therefore ignore timestamp skew.
Supported signature conventions
The connector delegates signature checks to
harn_vm::connectors::verify_hmac_signed(...), so it inherits the shared
verification rules from C-01:
- verify against the raw inbound bytes, not a reparsed body
- compare signatures in constant time
- enforce a timestamp window for timestamped schemes
- append signature failures to the
audit.signature_verifyevent-log topic
Supported variants:
- Standard Webhooks:
webhook-id,webhook-timestamp,webhook-signature: v1,<base64> - Stripe-style:
Stripe-Signature: t=<unix>,v1=<hex>[,v1=<hex>...] - GitHub-style:
X-Hub-Signature-256: sha256=<hex>
Normalized event fields
For successful deliveries the connector produces:
provider = "webhook"kindfromRawInbound.kind, thenX-GitHub-Event, then payloadtype/event, else"webhook"dedupe_keyfrom the provider-native delivery identifier:webhook-id, Stripe eventid, orX-GitHub-Deliverysignature_status = { state: "verified" }provider_payload = GenericWebhookPayload
GenericWebhookPayload.raw keeps parsed JSON when the body is JSON. When the
payload is not valid JSON, the connector preserves the bytes as:
{
"raw_base64": "<base64-encoded body>",
"raw_utf8": "optional utf-8 view"
}
GenericWebhookPayload.source comes from X-Webhook-Source when present, or
from the binding's optional webhook.source override.
Dedupe
If the trigger manifest declares dedupe_key, the connector records the
normalized event.dedupe_key in the trigger inbox before dispatch. Replays for
the same binding are dropped before handler execution, and the dedupe claim is
durable across orchestrator restarts for the configured retry retention window.
Activation and listener integration
The connector's activate() hook validates the binding config and reserves
unique match.path values across active bindings. The orchestrator listener
maps each incoming HTTP request path to the active trigger binding, passes the
original bytes through RawInbound, applies connector normalization, and then
appends accepted events to the dispatcher queue.
Direct normalize_inbound(...) calls remain useful for tests and embedding.
When more than one binding is active, callers must pass the selected
binding_id in RawInbound.metadata.binding_id so the connector can resolve
the configured secret and signature variant.
Notes and follow-up
- Signature failures are audited even when normalization returns an error.
- Production TLS and request-size limits are owned by the orchestrator listener and HTTP server layer, not by this connector.
- Provider-specific business logic should live in pure-Harn connector packages that compose this raw webhook substrate rather than adding more Rust provider-specific branches here.