refactor(webhook): establish canonical intake authority - #90995
refactor(webhook): establish canonical intake authority#90995andrexibiza wants to merge 1 commit into
Conversation
Centralize route/provider/verifier binding, provider-native retry identity, provider-scoped event extraction, and immutable intake-envelope construction behind one domain authority. Keep legacy header inference isolated to undeclared compatibility routes and never use timestamps as delivery identity. Refs NousResearch#90989 Interlocks NousResearch#90236 and NousResearch#85318.
298dc23 to
a9af5ef
Compare
Exact-head hosted verificationVerified the published head
The later Label-rerun records are skipped/cancelled orchestration noise; they do not supersede the successful exact-head product workflows above. This closes the PR body's outstanding “fresh hosted CI/Docker/Nix required” gate for the canonical webhook intake-authority slice. The composition boundary with #90236/#85318/#90304/#85640 remains exactly as documented; this comment does not claim terminal campaign integration. |
|
@teknium1 convergence review requested on exact head Revalidated now: GitHub reports open/non-draft/mergeable. Exact-head CI This should remain the canonical webhook domain/intake spine: provider namespace, route/provider binding, provider-native delivery identity, raw-body provenance, and immutable |
Architecture interlock locked in #91913This PR remains the active webhook delivery spine in the new machine-readable authority contract. The exact role is now tested as:
The conformance test fails if another PR displaces #90995 as the spine or if documentation/final assembly moves ahead of effect settlement. Contract PR: #91913 |
The security reasoning here is sound and well-documented: payload-derived delivery IDs accepted only for declared providers ( Concrete points:
The test suite is genuinely strong — immutability, alias canonicalization, precedence, and both fail-closed paths are all covered. |
Refs #90989.
Purpose
This is the first implementation slice of the webhook surface-convergence issue. It creates the domain authority that the existing webhook campaign lanes can compose onto instead of continuing to derive provider, delivery identity, event identity, and intake provenance independently.
Exact topology
5e32e3aecd2070e8245d9ae2c9ee257f547fcb71a9af5efcfeb6d81ae7be04421c7fcc8e872440faf174a69ead1a509be956207581eaccf927fe6665webhook.pygod-file rewriteThe branch was rebuilt after
mainadvanced during PR creation. The current head overlays only the two webhook-authority blobs onto the exact new main tree; no intervening upstream files are dropped.What this establishes
Canonical provider registry
gateway/platforms/webhook_contract.pyowns provider namespaces and the wire facts required by intake: aliases, provider-native delivery-ID headers, event headers, compatibility-detection headers, payload identity keys, allowed verifier modes, and default verifier mode.Provider identity and cryptographic verifier identity are related but deliberately not conflated. For example, an explicitly declared Chatwoot route can remain in the
chatwootprovider namespace while using a registered generic HMAC verifier. Impossible provider/verifier combinations fail closed.One route/provider binding decision
WebhookRouteConfig.bind()normalizes profile, provider, signature mode, enabled state, and event allowlist once. Explicit route authority cannot subsequently be replaced by attacker-controlled request headers.Undeclared historical routes retain one explicit compatibility bridge (
infer_legacy_provider) so migration can be staged without silently breaking existing installs. The compatibility result is recorded as provenance and is not a downstream authorization mechanism.Provider-native retry identity
resolve_delivery_identity()consumes only the already-bound provider namespace:X-GitHub-Deliverysvix-idwebhook-id/Idempotency-KeyX-Request-IDThere is no timestamp fallback. If no stable provider identity exists, deduplication authority is absent and the envelope receives a unique trace/session identity instead.
Payload
idis accepted only for explicitly declared providers, so an attacker cannot turn an undeclared/generic route into Stripe or Chatwoot by shaping the JSON body.Immutable intake envelope
WebhookEnvelopecarries the facts that downstream execution should consume exactly once:This creates the intended HTTP/domain boundary: later composition can move from
aiohttp.RequesttoWebhookEnvelopewithout re-deriving security or identity facts.Focused verification surface
tests/gateway/test_webhook_contract.pycovers immutable registry behavior, alias normalization, unknown provider/verifier rejection, provider/verifier disagreement, valid provider + generic-verifier composition, attacker-header resistance, provider-scoped event extraction, bounded legacy inference, provider-native identity precedence, absence of dedupe authority without a stable ID, timestamp non-identity, explicit-only payload IDs, scoped idempotency keys, profile mismatch, raw-body provenance, and recursive envelope immutability.Fresh hosted CI/Docker/Nix is required on
a9af5efc...; no sibling or historical green is inherited.Composition boundary
This PR intentionally does not rewrite the current
WebhookAdapterhandler yet. Doing that before the current Task 10/auth/session candidates converge would recreate the same multi-owner conflict #90989 is intended to eliminate.The next convergence slice should wire current intake/auth onto this contract, preserving #90236's body-hash/idempotency semantics and consuming #85318's verifier rather than copying either implementation again.