Class: Axn::Webhooks::Outbound::Config

Inherits:
Object
  • Object
show all
Extended by:
Configurable::Settings
Defined in:
lib/axn/webhooks/outbound/config.rb

Overview

The resolved, immutable outbound declaration. One per process (a single outbound block).

Defined Under Namespace

Classes: Resolution

Constant Summary collapse

DEFAULT_MAX_ATTEMPTS =
8
DEFAULT_BACKOFF =

Equal jitter (half fixed, half random): a fan-out event whose receiver is down would otherwise have every failing target retry in lockstep, converging on the same instant.

lambda do |attempt|
  base = [30 * (3**(attempt - 1)), 6 * 3600].min
  ((base / 2.0) + (rand * base / 2.0)).round
end
DEFAULT_OPEN_TIMEOUT =
5
DEFAULT_READ_TIMEOUT =
10
TIMEOUT_VALIDATE =

Forwarded straight to Net::HTTP (see Transport), which calls .zero?/compares on whatever it's given — an unvalidated non-Numeric (e.g. a String from ENV.fetch("OPEN_TIMEOUT")) would otherwise raise NoMethodError mid-delivery instead of failing at boot (Codex P2 finding).

->(v) { (v.is_a?(Numeric) && v.positive?) || "must be a positive Numeric" }
SAFE_TO_INSPECT =

Scalars a rejected target's #inspect is safe to show verbatim in redact_target below: a bare-value target (the common malformed cases -- nil, a wrong-type url, a stray Integer) carries no OTHER fields that could hide a credential. NOT String: a webhook URL commonly embeds a credential itself (HTTP Basic userinfo, a signed/token query param) -- see redact_target's dedicated String handling.

[Numeric, Symbol, NilClass, TrueClass, FalseClass].freeze
HASH_KEY_NAME =

A plausible field-name typo (:secret, :api_key -- what Subscriber.coerce's own "unknown key(s)" message exists to surface) is a short, simple identifier -- used by redact_hash_key below to decide whether a Hash row's key is safe to show as-is.

/\A[A-Za-z_][A-Za-z0-9_]{0,49}\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(signer:, events:, default_subscribers:, max_attempts:, backoff:, transport:, vendor: nil, user_agent: nil, open_timeout: nil, read_timeout: nil, allowed_hosts: nil, allow_url: nil, headers: nil) ⇒ Config

rubocop:disable Metrics/ParameterLists -- one kwarg per DSL setting, mirroring DSL#__config__'s call site 1:1; a Hash-options refactor would ripple through every caller for no real gain.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/axn/webhooks/outbound/config.rb', line 109

def initialize(signer:, events:, default_subscribers:, max_attempts:, backoff:, transport:,
               vendor: nil, user_agent: nil, open_timeout: nil, read_timeout: nil,
               allowed_hosts: nil, allow_url: nil, headers: nil)
  # rubocop:enable Metrics/ParameterLists
  @signer = signer
  @events = events # { Symbol => { to:, type:, vendor: } }
  @default_subscribers = default_subscribers

  self.max_attempts = max_attempts unless max_attempts.nil?
  self.backoff = backoff unless backoff.nil?
  self.transport = transport unless transport.nil?
  self.vendor = vendor unless vendor.nil?
  self.user_agent = user_agent unless user_agent.nil?
  self.open_timeout = open_timeout unless open_timeout.nil?
  self.read_timeout = read_timeout unless read_timeout.nil?
  self.allowed_hosts = allowed_hosts unless allowed_hosts.nil?
  self.headers = headers unless headers.nil?
  self.allow_url = allow_url unless allow_url.nil?

  @events.each { |name, spec| validate_event!(name, spec) }

  deep_freeze!
end

Instance Attribute Details

#signerObject (readonly)

Returns the value of attribute signer.



133
134
135
# File 'lib/axn/webhooks/outbound/config.rb', line 133

def signer
  @signer
end

Class Method Details

.url_problem(url) ⇒ Object

The problem with url as an outbound target, or nil when there is none. A predicate rather than a raiser, because its two callers disagree on the error class: a declaration mistake at boot is an ArgumentError (see validate_url!), while a bad one-off emit(to:) URL is an Axn::Webhooks::Error a caller may rescue at runtime (see Outbound::Emit). Shape only (no host policy) -- Outbound::Emit#resolve_targets applies allowed_hosts/ allow_url itself via TargetPolicy.check! for the one-off to: override; this predicate stays a pure URL-shape check so validate_url!'s narrower boot-time contract doesn't drift.



100
101
102
103
104
105
# File 'lib/axn/webhooks/outbound/config.rb', line 100

def self.url_problem(url)
  TargetPolicy.parse_url!(url)
  nil
rescue Axn::Webhooks::InvalidTarget => e
  e.message
end

Instance Method Details

#eventsObject



135
# File 'lib/axn/webhooks/outbound/config.rb', line 135

def events = @events.keys

#resolve_subscribers(event) ⇒ Object

A DECLARED per-event to: always wins, even when it resolves to zero targets — a static Array as-is (including []), or a lambda ->(event){…} invoked (arity-aware, matching Resolvers.resolve) and its result wrapped in Array (nil -> []). The block-level subscribers resolver is ONLY consulted when the event declared no to: at all (spec.nil?) — never as a fallback for a declared resolver returning nil.

A static to: Array is validated ONCE, at boot (validate_event!) -- so its entries are only ever re-COERCED into Subscribers here, never re-checked against TargetPolicy. Re-running it on every resolution would be wasted work for a target that can never change, and would actually BREAK a stateful/rate-limited allow_url predicate: it could reject an already-boot-validated static target later, contradicting the documented once-at-boot contract (Codex P2 finding). Every OTHER raw entry -- from a callable to: or the subscribers fallback, neither checkable at boot since they depend on runtime state -- goes through the SAME TargetPolicy a static Array was checked against, so a runtime resolver can never see a looser bar than a hand-written Array does.



167
168
169
170
171
172
173
# File 'lib/axn/webhooks/outbound/config.rb', line 167

def resolve_subscribers(event)
  spec = fetch(event)
  return static_resolution(spec) if spec[:to].is_a?(Array)

  raw = spec[:to].nil? ? call_resolver(@default_subscribers, event) : resolve_to(spec[:to], event)
  check_targets(raw)
end

#targets_for(event) ⇒ Object

Back-compat convenience for callers that only want the resolved URLs and don't care about a malformed row -- e.g. today's Emit fan-out. Silently drops rejections; a caller that needs to know about (or report) them wants resolve_subscribers directly. Frozen: for a static to: Array, resolve_subscribers used to return the SAME frozen Array deep_freeze! already produced; going through Subscriber/.map builds a fresh one, which .map never freezes on its own -- targets_for's own immutability contract (asserted in config_immutability_spec.rb) has to be re-established here explicitly.



182
183
184
# File 'lib/axn/webhooks/outbound/config.rb', line 182

def targets_for(event)
  resolve_subscribers(event).subscribers.map(&:url).freeze
end

#vendor_for(event) ⇒ Object

A per-event vendor: overrides the block-level default; same precedence as type:.



142
143
144
# File 'lib/axn/webhooks/outbound/config.rb', line 142

def vendor_for(event)
  fetch(event)[:vendor] || vendor
end

#wire_type(event) ⇒ Object



137
138
139
# File 'lib/axn/webhooks/outbound/config.rb', line 137

def wire_type(event)
  (fetch(event)[:type] || event).to_s
end