Class: Axn::Webhooks::Inbound::Endpoint
- Inherits:
-
Object
- Object
- Axn::Webhooks::Inbound::Endpoint
- Defined in:
- lib/axn/webhooks/inbound/endpoint.rb
Overview
A registered inbound webhook endpoint. Verifies a request's signature, dispatches the (verified, parsed) event to a handler Axn, and maps the pipeline's outcome to an HTTP Response. Challenge (GET) and Rack mount arrive in a later phase.
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
-
#call(env) ⇒ Object
The Rack app entry point (spec: mount-first packaging).
-
#challenge_required?(request) ⇒ Boolean
Is this request an authentication attempt at all? When it isn't, there is nothing to verify — it's a protocol precondition, not a failed verification — and #to_response answers with the challenge without invoking Verify (PRO-3148).
-
#challenge_response(request) ⇒ Object
The GET branch (spec: the mount owns the whole path, every verb).
-
#handle(request) ⇒ Object
Full pipeline: verify, then (if a dispatch is declared and verification passed) parse + route to the handler.
-
#initialize(name:, verifier:, dispatch: nil, respond: nil, static_respond: nil, challenge: nil, unauthorized_headers: nil, challenge_required: nil) ⇒ Endpoint
constructor
A new instance of Endpoint.
-
#to_response(request) ⇒ Object
The staged HTTP outcome mapping (spec: "Respond + staged outcome model").
-
#unauthorized_headers ⇒ Object
Headers attached to the 401 a verify failure produces.
-
#verify(request) ⇒ Object
Verify the request's signature.
Constructor Details
#initialize(name:, verifier:, dispatch: nil, respond: nil, static_respond: nil, challenge: nil, unauthorized_headers: nil, challenge_required: nil) ⇒ Endpoint
Returns a new instance of Endpoint.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 10 def initialize(name:, verifier:, dispatch: nil, respond: nil, static_respond: nil, challenge: nil, unauthorized_headers: nil, challenge_required: nil) if dispatch && dispatch[:mode] == :async && respond raise Axn::Webhooks::Error, "inbound endpoint `#{name}` declares a custom `respond` but explicit `dispatch mode: :async` " \ "can't produce a handler_result for it to read — use `mode: :sync` (or omit mode) or drop the respond block" end if respond && static_respond raise Axn::Webhooks::Error, "inbound endpoint `#{name}` declares both `respond` and `static_respond` — declare only one; " \ "`respond` reads the handler's result, `static_respond` doesn't and renders on every non-error outcome" end @name = name.to_sym @verifier = verifier @dispatch = dispatch @respond = respond @static_respond = static_respond @challenge = challenge @unauthorized_headers = @challenge_required = challenge_required validate_challenge! end |
Instance Attribute Details
#name ⇒ Object (readonly)
Returns the value of attribute name.
36 37 38 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 36 def name @name end |
Instance Method Details
#call(env) ⇒ Object
The Rack app entry point (spec: mount-first packaging). Inbound[:vendor] (this object)
is directly mount-able in Rails routes.rb or run-able in a bare Rack::Builder — the
mount owns the whole path and every verb: POST -> #to_response, GET -> #challenge_response,
anything else -> 405. Named call, deliberately reserved since Phase 3 (see #handle).
130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 130 def call(env) built = BuildRequest.call(env:, vendor: @name) return Response.new(status: 500).to_rack unless built.ok? request = built.request response = case request.http_method when "POST" then to_response(request) when "GET" then challenge_response(request) else Response.new(status: 405) end response.to_rack end |
#challenge_required?(request) ⇒ Boolean
Is this request an authentication attempt at all? When it isn't, there is nothing to verify — it's a protocol precondition, not a failed verification — and #to_response answers with the challenge without invoking Verify (PRO-3148). Under a two-legged scheme like RFC 7617 Basic auth a reactive client sends one such request per successful webhook, so recording them as verify failures made the highest-volume outcome on a healthy endpoint a recorded failure, and a cross-vendor verify-failure monitor unusable without knowing which vendors happen to use Basic auth.
False unless something says otherwise, so the signature strategies — which have no challenge to offer and no second leg to wait for — are untouched: no predicate means no ChallengeRequired call either, not merely a false answer from one.
Note this is NOT the challenge declaration (that's the vendor's GET handshake, see
#challenge_response). Same word, different protocol: this one is the 401 kind.
68 69 70 71 72 73 74 75 76 77 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 68 def challenge_required?(request) predicate = challenge_predicate return false unless predicate # Inside an Axn boundary: the predicate is request-dependent code the gem doesn't own, and # it runs ahead of every other boundary on the POST path. A crash settles not-ok and is read # as "can't tell" -> verify normally (see ChallengeRequired for why that's the safe answer). checked = ChallengeRequired.call(request:, predicate:, vendor: @name) checked.ok? && checked.required end |
#challenge_response(request) ⇒ Object
The GET branch (spec: the mount owns the whole path, every verb). Testable without a Rack env, mirroring #verify/#handle/#to_response.
117 118 119 120 121 122 123 124 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 117 def challenge_response(request) return Response.new(status: 405) unless @challenge # The Challenge axn computes the exact Response (200 echo / 403 guard-fail / 400 nil). # Only a raising resolver/guard makes it not-ok -> a reported 500. result = Challenge.call(request:, resolver: @challenge[:resolver], guard: @challenge[:guard], vendor: @name) result.ok? ? result.response : Response.new(status: 500) end |
#handle(request) ⇒ Object
Full pipeline: verify, then (if a dispatch is declared and verification passed) parse + route to the handler. Returns the final Axn::Result.
87 88 89 90 91 92 93 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 87 def handle(request) verified = verify(request) return verified unless verified.ok? && @dispatch Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse], mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name) end |
#to_response(request) ⇒ Object
The staged HTTP outcome mapping (spec: "Respond + staged outcome model"). Verify and
dispatch are mapped in separate branches — deliberately NOT a single outcome->status
rule, because a verify failure (401) and a handler business fail! (2xx) are both
outcome.failure? but mean opposite things at the HTTP layer.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 99 def to_response(request) # Ahead of verify, deliberately: a request that isn't an authentication attempt gets the # challenge rather than a recorded verify failure (see #challenge_required?). Same 401 on # the wire, and it still can't reach a handler — strictly safer than the `done!` that # would settle this leg as a *success*. return Response.new(status: 401, headers: ) if challenge_required?(request) verified = verify(request) return Response.new(status: 401, headers: ) unless verified.ok? return default_ack unless @dispatch dispatched = Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse], mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name) response_for(dispatched) end |
#unauthorized_headers ⇒ Object
Headers attached to the 401 a verify failure produces. Empty for the signature
strategies — there is nothing for a signing client to be challenged with — but
mandatory for HTTP Basic auth (RFC 7617), where a client that doesn't authenticate
preemptively sends its first request bare and repeats it with credentials only after a
401 carrying WWW-Authenticate. Without this the second leg never comes and every
request from such a client is dropped, uniformly and silently.
An explicit unauthorized_headers declaration wins, so a custom verify block can
supply its own challenge; otherwise the verifier speaks for itself.
47 48 49 50 51 52 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 47 def return @unauthorized_headers if @unauthorized_headers return @verifier. if @verifier.respond_to?(:unauthorized_headers) {} end |
#verify(request) ⇒ Object
Verify the request's signature. Returns an Axn::Result: ok? when verified, a failure on mismatch, an exception if the verifier raises.
81 82 83 |
# File 'lib/axn/webhooks/inbound/endpoint.rb', line 81 def verify(request) Verify.call(request:, verifier: @verifier, vendor: @name) end |