Class: Axn::Webhooks::Inbound::DSL

Inherits:
Object
  • Object
show all
Defined in:
lib/axn/webhooks/inbound/dsl.rb

Overview

Receiver for an inbound block: captures declarations (Phase 2: verify) and exposes request resolvers. Later phases add dispatch/challenge/respond here.

Constant Summary collapse

INHERITED_IVARS =

Every declaration a child endpoint inherits. Deliberately excludes @child_endpoints (nesting is one level deep) and @nested.

Also excludes @dispatch_spec, which cannot be inherited because a parent declaring dispatch alongside endpoint blocks is rejected at registration (see Axn::Webhooks.inbound). Listing it would be inert, and it would advertise a shared-dispatch feature that does not work anyway: dispatch captures ONE spec hash, so a child re-declaring it replaces the parent's wholesale — there is no partial override, and a parent dispatch every child copies verbatim leaves the children differing by nothing (Codex review asked for this; declined for that reason plus the migration hazard the guard exists to catch — silently losing Inbound out from under a mounted route).

%i[
  @verify_spec @unauthorized_headers @challenge_required
  @respond_block @static_respond_block @challenge_spec
].freeze

Instance Method Summary collapse

Instance Method Details

#__challenge__Object

Internal: the captured { resolver:, guard: } challenge declaration, or nil if none.



230
# File 'lib/axn/webhooks/inbound/dsl.rb', line 230

def __challenge__ = @challenge_spec

#__challenge_required__Object

Internal: the declared challenge-required predicate, or nil to ask the verifier.



236
# File 'lib/axn/webhooks/inbound/dsl.rb', line 236

def __challenge_required__ = @challenge_required

#__child_dsl__(block) ⇒ Object

Internal: a fresh DSL seeded with this one's captured declarations, with block evaluated against it — so a child inherits everything and overrides by re-declaring.

Copies the ivars rather than re-instance_execing the parent block per child (the obvious alternative): replaying the parent block would re-run any side effects in it, and would re-enter endpoint recursively, registering each child once per sibling.



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/axn/webhooks/inbound/dsl.rb', line 166

def __child_dsl__(block)
  child = self.class.new
  inherited = INHERITED_IVARS.select { |ivar| instance_variable_defined?(ivar) }
  inherited.each { |ivar| child.instance_variable_set(ivar, instance_variable_get(ivar)) }
  # Recorded so `respond`/`static_respond` can tell an inherited value from one declared in
  # the child's own block, and clear only the former.
  child.instance_variable_set(:@inherited_ivars, inherited.dup)
  child.instance_variable_set(:@nested, true)
  child.instance_exec(&block)
  child
end

#__children__Object

Internal: declared child endpoints, name => block. Empty for a plain inbound block.



154
# File 'lib/axn/webhooks/inbound/dsl.rb', line 154

def __children__ = @child_endpoints || {}

#__dispatch__Object

Internal: build the { router:, parse:, mode: } dispatch config, or nil if none declared.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/axn/webhooks/inbound/dsl.rb', line 205

def __dispatch__
  return nil unless @dispatch_spec

  spec = @dispatch_spec
  unless %i[auto sync async].include?(spec[:mode])
    raise Axn::Webhooks::Error, "dispatch mode: must be :sync, :async, or :auto (got #{spec[:mode].inspect})"
  end

  unless spec[:unparseable_status].nil? || Response.valid_status?(spec[:unparseable_status])
    raise Axn::Webhooks::Error,
          "dispatch unparseable_status: must be an Integer HTTP status between 200 and 599 " \
          "(got #{spec[:unparseable_status].inspect})"
  end

  router = Router.new(to: spec[:to], on: spec[:on], otherwise: spec[:otherwise], via: spec[:via])
  { router:, parse: Parsers.build(spec[:parse]), mode: spec[:mode], unparseable_status: spec[:unparseable_status] }
end

#__dispatch_declared?Boolean

Internal: whether dispatch was declared directly on THIS DSL. Read instead of __dispatch__ so the parent-with-children check doesn't build a Router just to ask.

Returns:

  • (Boolean)


158
# File 'lib/axn/webhooks/inbound/dsl.rb', line 158

def __dispatch_declared? = !@dispatch_spec.nil?

#__respond__Object

Internal: the captured respond block, or nil if none declared.



224
# File 'lib/axn/webhooks/inbound/dsl.rb', line 224

def __respond__ = @respond_block

#__static_respond__Object

Internal: the captured static_respond block, or nil if none declared.



227
# File 'lib/axn/webhooks/inbound/dsl.rb', line 227

def __static_respond__ = @static_respond_block

#__unauthorized_headers__Object

Internal: the declared 401 headers, or nil to let the verifier speak for itself.



233
# File 'lib/axn/webhooks/inbound/dsl.rb', line 233

def __unauthorized_headers__ = @unauthorized_headers

#__verifier__Object

Internal: build the verifier callable from the captured declaration. For challenge-only endpoints (no dispatch, no verify declared), return a no-op verifier that always succeeds — a challenge-only endpoint just handshakes the GET and 200-acks any POST, so there's no unverified processing to guard against. verify is REQUIRED whenever dispatch is declared — dispatching an unverified webhook would run the handler on an unauthenticated request.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/axn/webhooks/inbound/dsl.rb', line 184

def __verifier__
  unless @verify_spec
    # Nothing declared at all: bare endpoint, always an error.
    raise Axn::Webhooks::Error, "inbound endpoint declared no `verify`" if @dispatch_spec.nil? && @challenge_spec.nil?

    # `dispatch` without `verify` is unsafe regardless of whether `challenge` is also present.
    if @dispatch_spec
      raise Axn::Webhooks::Error,
            "inbound endpoint with `dispatch` must declare `verify` — dispatching an unverified webhook is unsafe"
    end

    # Challenge-only endpoint (no dispatch): return a no-op verifier.
    return ->(_request) { true }
  end

  raise Axn::Webhooks::Error, "inbound endpoint `verify` needs a strategy or a block" if @verify_spec[:strategy].nil? && @verify_spec[:block].nil?

  Verifiers.build(**@verify_spec)
end

#async(call, **opts) ⇒ Object

Dispatch-map sugar: async("H") == { call: "H", async: true }; sync forces sync. Callable inside a dispatch to: { … } map because the inbound block is instance_exec'd against this DSL. Extra kwargs (e.g. with:) pass through: async("H", with: ->(e){ … }). **opts is spread FIRST so the fixed mode and the positional handler always win — a splatted shared options hash carrying :async/:call can never silently flip the mode or retarget the handler (the helper's name is its contract).



121
# File 'lib/axn/webhooks/inbound/dsl.rb', line 121

def async(call, **opts) = { **opts, call:, async: true }

#challenge(resolver, if: nil) ⇒ Object

challenge ->(req){ req.params } — Nylas challenge ->(req){ req.params }, if: ->(req){ ... } — Meta



103
104
105
106
107
108
# File 'lib/axn/webhooks/inbound/dsl.rb', line 103

def challenge(resolver, if: nil)
  # `if:` shadows Ruby's `if` keyword inside this method body — must read it back via
  # binding.local_variable_get, not a bare `if` reference (that's a syntax trap, not a var).
  guard = binding.local_variable_get(:if)
  @challenge_spec = { resolver:, guard: }
end

#challenge_required(&block) ⇒ Object

challenge_required { |req| req.header("Authorization").to_s.empty? }

Declares which requests are not authentication attempts at all, and so get the challenge (401 + unauthorized_headers) instead of being run through verify and recorded as verify failures. verify :basic_auth answers this itself; declare it only for a custom verify block that wraps a two-legged scheme the gem can't see through — the shape buyout's Twilio routes use, where the BasicAuth verifier sits inside a block:

challenge_required { |req| my_basic_auth.challenge_required?(req) }


48
49
50
# File 'lib/axn/webhooks/inbound/dsl.rb', line 48

def challenge_required(&block)
  @challenge_required = block
end

#claim_ownership(ivar) ⇒ Object

Marks an ivar as belonging to THIS block from here on. Without it, a child that re-declared respond left @respond_block still listed as inherited, so a following static_respond discarded the child's OWN block and the pair was accepted — exactly the same-block conflict the discard rule is supposed to keep raising (Codex review).



151
# File 'lib/axn/webhooks/inbound/dsl.rb', line 151

def claim_ownership(ivar) = @inherited_ivars&.delete(ivar)

#discard_inherited(ivar) ⇒ Object

Drops an ivar this DSL INHERITED from a parent endpoint container, leaving one declared in this very block untouched. Backs the mutually-exclusive renderer override above.



140
141
142
143
144
145
# File 'lib/axn/webhooks/inbound/dsl.rb', line 140

def discard_inherited(ivar)
  return unless @inherited_ivars&.include?(ivar)

  instance_variable_set(ivar, nil)
  @inherited_ivars.delete(ivar)
end

#dispatch(to: nil, on: nil, otherwise: nil, via: nil, parse: :json, mode: :auto, unparseable_status: nil) ⇒ Object

dispatch to: "Handler" | dispatch on: ->(e)…, to: map, otherwise:, via: | parse: | mode: unparseable_status: overrides Axn::Webhooks.config.unparseable_status for THIS endpoint — it belongs here, next to parse:, because it only describes what happens when that parse fails, and because the right value is a fact about one vendor's retry policy (PRO-3143). rubocop:disable-next Naming/MethodParameterName



57
58
59
# File 'lib/axn/webhooks/inbound/dsl.rb', line 57

def dispatch(to: nil, on: nil, otherwise: nil, via: nil, parse: :json, mode: :auto, unparseable_status: nil)
  @dispatch_spec = { to:, on:, otherwise:, via:, parse:, mode:, unparseable_status: }
end

#endpoint(name, &block) ⇒ Object

endpoint(:events) { dispatch … } — declares a CHILD endpoint that inherits everything the parent block declared and may override any of it by re-declaring. One inbound :slack block with two endpoint blocks registers Inbound and Inbound; the parent itself registers nothing.

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
# File 'lib/axn/webhooks/inbound/dsl.rb', line 128

def endpoint(name, &block)
  raise ArgumentError, "`endpoint #{name.inspect}` requires a block" unless block
  raise ArgumentError, "`endpoint #{name.inspect}` cannot be nested inside another `endpoint` — one level only" if @nested

  @child_endpoints ||= {}
  raise ArgumentError, "duplicate `endpoint #{name.inspect}` in the same inbound block" if @child_endpoints.key?(name.to_sym)

  @child_endpoints[name.to_sym] = block
end

#header(name) ⇒ Object



110
# File 'lib/axn/webhooks/inbound/dsl.rb', line 110

def header(name) = Resolvers.header(name)

#paramsObject



112
# File 'lib/axn/webhooks/inbound/dsl.rb', line 112

def params       = Resolvers.params

#raw_bodyObject



111
# File 'lib/axn/webhooks/inbound/dsl.rb', line 111

def raw_body     = Resolvers.raw_body

#respond(&block) ⇒ Object

respond { |handler_result| text("...") } — maps a genuine handler success to a Response. Every other outcome (ack, business fail!, verify failure/exception, or a no-dispatch endpoint) always gets the default bare ack (or a declared static_respond body — see below), regardless of this declaration — see Endpoint#to_response.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/axn/webhooks/inbound/dsl.rb', line 65

def respond(&block)
  # Checked BEFORE the discard below: without a block this stored nil after clearing the
  # inherited alternative, booting an endpoint with no renderer at all — an undocumented way
  # to un-declare a parent's renderer, and a typo that silently downgraded responses to bare
  # acks (Codex review).
  raise Axn::Webhooks::Error, "inbound endpoint's `respond` requires a block" unless block

  # Endpoint rejects having both renderers set, and a child inherits BOTH ivars — so
  # overriding an inherited `static_respond` with a `respond` has to clear it, or the child
  # raises. Clears only an INHERITED one: declaring both in the same block stays an error
  # rather than silently becoming last-one-wins (Codex review).
  discard_inherited(:@static_respond_block)
  @respond_block = block
  claim_ownership(:@respond_block)
end

#static_respond(&block) ⇒ Object

static_respond { text("...") } — a body that does NOT read the handler result (block takes zero args, unlike respond's |handler_result|), so it renders on every non-error outcome: sync success, async enqueue, otherwise: :ack, and business fail! — see Endpoint#default_ack. Mutually exclusive with respond (Endpoint#initialize raises if both are declared) and never forces sync dispatch (Dispatch#async? never reads it).



86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/axn/webhooks/inbound/dsl.rb', line 86

def static_respond(&block)
  raise Axn::Webhooks::Error, "inbound endpoint's `static_respond` requires a block" unless block

  if block.parameters.any?
    raise Axn::Webhooks::Error,
          "inbound endpoint's static_respond block must take no arguments (it never reads the " \
          "handler's result, unlike respond) — got a parameter; use `respond` instead if you need " \
          "to read the handler's result"
  end

  discard_inherited(:@respond_block) # see `respond` — cross-form override, inherited only
  @static_respond_block = block
  claim_ownership(:@static_respond_block)
end

#sync(call, **opts) ⇒ Object



122
# File 'lib/axn/webhooks/inbound/dsl.rb', line 122

def sync(call, **opts)  = { **opts, call:, async: false }

#unauthorized_headers(headers) ⇒ Object

unauthorized_headers "WWW-Authenticate" => %(Basic realm="Webhook")

Headers to attach to the 401 a verify failure produces. verify :basic_auth supplies this itself; declare it only for a custom verify block that has to challenge a client into retrying with credentials (see Endpoint#unauthorized_headers).



35
36
37
# File 'lib/axn/webhooks/inbound/dsl.rb', line 35

def unauthorized_headers(headers)
  @unauthorized_headers = headers
end

#urlObject



113
# File 'lib/axn/webhooks/inbound/dsl.rb', line 113

def url          = Resolvers.url

#verify(strategy = nil, **opts, &block) ⇒ Object

verify :hmac, **opts | verify :standard_webhooks, **opts | verify { |req| ... }



26
27
28
# File 'lib/axn/webhooks/inbound/dsl.rb', line 26

def verify(strategy = nil, **opts, &block)
  @verify_spec = { strategy:, opts:, block: }
end