axn-webhooks 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6afafb259befddc2850cf02b18286b55948c9e061c1e07377203dd5430c75f77
4
- data.tar.gz: 79bf28d109278e3c6a63a22093758e32f02496e9747edebcf33cbaf8c8ec0635
3
+ metadata.gz: c8b8fbe499b148a37eeebd9a111dd27343dd39618c11ebf00c2d01b0c5a21a30
4
+ data.tar.gz: 99a2eb19546c6f49ec060811cfa83d0b063e0bf1dc77a8156843c4da6197e1fe
5
5
  SHA512:
6
- metadata.gz: ccea01ad6d3b8f9da8347466c715b843e87d3a638f6851d025dba9258e83256e8d51f00ad4b065cf3e1c6a5f8a12f8640c2b883f0dda77065ffa0c3f0699508f
7
- data.tar.gz: a1460ca78f2eeb544e90728e4c559d2c1e473cd4ddd5ba197aa3cc8d2a92f93d42e325628baa3480dd012e848fb34a0ab53dfd3132351bd3f0aae0409aaff5aa
6
+ metadata.gz: aa876a263ff34769f2e442483b67709a62efeaa3bd1f270b5fd72329497a98726098213d0644458f1f813fc4d7d2299c1c2d7c47f5ddcfbf8086add4d1dd2619
7
+ data.tar.gz: 85dd9e4689f05f29cca902ac947d93db1ec76323ba1ab47427468d3918c6bb5805ecdbcdbb09aa44cc30ebe5832ef8c9c864bd3adb125b941053a26078c9c1dd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ All notable changes to this project are documented here. This project adheres to
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [0.1.1] - 2026-09-03
11
+
12
+ _Prepared, not yet tagged — the version is cut immediately on merge. Update this date if that slips._
13
+
14
+ ### Observability
15
+
16
+ - Bumped the `axn` floor to `>= 0.1.0-alpha.6` for `Axn::Extensions::InvokedVia`. Every public
17
+ `Endpoint` entrypoint — `#call` (the Rack app), `#handle`, `#to_response`, `#challenge_response`,
18
+ `#verify`, and `#challenge_required?` (the controller-driven alternatives README/DESIGN-NOTES
19
+ document alongside mounting) — now wraps itself in `InvokedVia.with(:webhooks)`, so every axn in a
20
+ request's call tree — `BuildRequest`,
21
+ `ChallengeRequired`, `Verify`, `Dispatch`, `Respond`/`StaticRespond`, `Challenge`, and the consuming
22
+ app's own handler axn — is stamped with an `invoked_via: :webhooks` dimension regardless of which
23
+ entrypoint a caller uses, with no per-class opt-in required (unlike the
24
+ [vendor facet](#per-vendor-observability), which is declared per-class and threaded through
25
+ explicitly). Wraps nest safely, so calling one entrypoint from another (e.g. `#call` -> `#to_response`
26
+ -> `#verify`) re-stamps the same value harmlessly.
27
+ - Known gap, not addressed here: a route dispatched via `:auto`/`:async` mode enqueues a background
28
+ job from inside the wrapped tree, so the *enqueue* is tagged — but the job's *performed* execution
29
+ runs in a separate process, outside the ambient stamp's scope, so the handler's own execution
30
+ metrics won't carry `invoked_via` in that mode.
31
+
10
32
  ## [0.1.0] - 2026-08-24
11
33
 
12
34
  _Prepared, not yet tagged — the version is cut immediately on merge. Update this date if that slips._
data/README.md CHANGED
@@ -621,6 +621,28 @@ This governs the **vendor** facet only. The [`reason` dimension](#why-verificati
621
621
  stamped — it's a closed enum, so there's no cardinality decision to defer. Group by `reason`, filter
622
622
  by `vendor`.
623
623
 
624
+ ## Entry-point attribution
625
+
626
+ Every inbound request — however it arrives, the mounted Rack app (`#call`) or one of the
627
+ controller-driven entrypoints (`#verify`, `#handle`, `#to_response`, `#challenge_response`,
628
+ `#challenge_required?`; see [Mounting](#mounting)) — is wrapped in [`axn`](https://github.com/teamshares/axn)'s
629
+ `Axn::Extensions::InvokedVia.with(:webhooks)`. Each entrypoint carries its own wrap (they nest safely,
630
+ so `#call` calling into `#to_response` calling into `#verify` just re-stamps the same value), which
631
+ stamps an `invoked_via: "webhooks"` dimension on the *whole* call tree reached from wherever a caller
632
+ enters: `BuildRequest`, `ChallengeRequired`, `Verify`, `Dispatch`, `Respond`/`StaticRespond`,
633
+ `Challenge`, and — with no opt-in required — the handler axn your `dispatch` block routes to. Unlike
634
+ the [vendor facet](#per-vendor-observability) above, there's no config flag: it's always on, and no
635
+ per-class declaration is needed, because `InvokedVia` is ambient rather than a value threaded through
636
+ each `.call`.
637
+
638
+ This lets a Datadog/OTel dashboard separate webhook-driven traffic from an Axn class's other callers
639
+ without that class knowing it's mounted behind a webhook at all.
640
+
641
+ One gap: a route dispatched with `mode: :auto`/`:async` enqueues the handler as a background job from
642
+ *inside* the wrapped call tree, so the enqueue itself is tagged — but the job's **performed**
643
+ execution runs in a separate process, outside the ambient stamp's scope, so the handler's own
644
+ execution metrics won't carry `invoked_via` in that mode.
645
+
624
646
  ---
625
647
 
626
648
  # Outbound
@@ -65,62 +65,84 @@ module Axn
65
65
  #
66
66
  # Note this is NOT the `challenge` declaration (that's the vendor's GET handshake, see
67
67
  # #challenge_response). Same word, different protocol: this one is the 401 kind.
68
+ #
69
+ # DESIGN-NOTES.md documents this as public for controllers driving #verify/#handle
70
+ # themselves, so — like them — it carries its own InvokedVia wrap (Codex review, PR #31).
68
71
  def challenge_required?(request)
69
72
  predicate = challenge_predicate
70
73
  return false unless predicate
71
74
 
72
- # Inside an Axn boundary: the predicate is request-dependent code the gem doesn't own, and
73
- # it runs ahead of every other boundary on the POST path. A crash settles not-ok and is read
74
- # as "can't tell" -> verify normally (see ChallengeRequired for why that's the safe answer).
75
- checked = ChallengeRequired.call(request:, predicate:, vendor: @name)
76
- checked.ok? && checked.required
75
+ Axn::Extensions::InvokedVia.with(:webhooks) do
76
+ # Inside an Axn boundary: the predicate is request-dependent code the gem doesn't own, and
77
+ # it runs ahead of every other boundary on the POST path. A crash settles not-ok and is
78
+ # read as "can't tell" -> verify normally (see ChallengeRequired for why that's safe).
79
+ checked = ChallengeRequired.call(request:, predicate:, vendor: @name)
80
+ checked.ok? && checked.required
81
+ end
77
82
  end
78
83
 
79
84
  # Verify the request's signature. Returns an Axn::Result: ok? when verified,
80
85
  # a failure on mismatch, an exception if the verifier raises.
86
+ #
87
+ # A standalone entrypoint too (README: "drive it yourself from a controller"), so it carries
88
+ # its own InvokedVia wrap — nests harmlessly when reached via #handle/#to_response/#call, which
89
+ # already wrap themselves.
81
90
  def verify(request)
82
- Verify.call(request:, verifier: @verifier, vendor: @name)
91
+ Axn::Extensions::InvokedVia.with(:webhooks) { Verify.call(request:, verifier: @verifier, vendor: @name) }
83
92
  end
84
93
 
85
94
  # Full pipeline: verify, then (if a dispatch is declared and verification passed)
86
95
  # parse + route to the handler. Returns the final Axn::Result.
96
+ #
97
+ # A second, independent entrypoint alongside #call (the Rack app) — not called from it, so it
98
+ # carries its own Axn::Extensions::InvokedVia wrap rather than inheriting one.
87
99
  def handle(request)
88
- verified = verify(request)
89
- return verified unless verified.ok? && @dispatch
100
+ Axn::Extensions::InvokedVia.with(:webhooks) do
101
+ verified = verify(request)
102
+ next verified unless verified.ok? && @dispatch
90
103
 
91
- Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse],
92
- mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name)
104
+ Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse],
105
+ mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name)
106
+ end
93
107
  end
94
108
 
95
109
  # The staged HTTP outcome mapping (spec: "Respond + staged outcome model"). Verify and
96
110
  # dispatch are mapped in separate branches — deliberately NOT a single outcome->status
97
111
  # rule, because a verify failure (401) and a handler business fail! (2xx) are both
98
112
  # `outcome.failure?` but mean opposite things at the HTTP layer.
113
+ #
114
+ # Also a standalone entrypoint (README: "drive it yourself from a controller") reachable
115
+ # without going through #call, so it carries its own InvokedVia wrap (Codex review, PR #31).
99
116
  def to_response(request)
100
- # Ahead of verify, deliberately: a request that isn't an authentication attempt gets the
101
- # challenge rather than a recorded verify failure (see #challenge_required?). Same 401 on
102
- # the wire, and it still can't reach a handler strictly safer than the `done!` that
103
- # would settle this leg as a *success*.
104
- return Response.new(status: 401, headers: unauthorized_headers) if challenge_required?(request)
105
-
106
- verified = verify(request)
107
- return Response.new(status: 401, headers: unauthorized_headers) unless verified.ok?
108
- return default_ack unless @dispatch
109
-
110
- dispatched = Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse],
111
- mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name)
112
- response_for(dispatched)
117
+ Axn::Extensions::InvokedVia.with(:webhooks) do
118
+ # Ahead of verify, deliberately: a request that isn't an authentication attempt gets the
119
+ # challenge rather than a recorded verify failure (see #challenge_required?). Same 401 on
120
+ # the wire, and it still can't reach a handler — strictly safer than the `done!` that
121
+ # would settle this leg as a *success*.
122
+ next Response.new(status: 401, headers: unauthorized_headers) if challenge_required?(request)
123
+
124
+ verified = verify(request)
125
+ next Response.new(status: 401, headers: unauthorized_headers) unless verified.ok?
126
+ next default_ack unless @dispatch
127
+
128
+ dispatched = Dispatch.call(request:, router: @dispatch[:router], parse: @dispatch[:parse],
129
+ mode: @dispatch[:mode], respond_declared: !@respond.nil?, vendor: @name)
130
+ response_for(dispatched)
131
+ end
113
132
  end
114
133
 
115
134
  # The GET branch (spec: the mount owns the whole path, every verb). Testable without a Rack
116
- # env, mirroring #verify/#handle/#to_response.
135
+ # env, mirroring #verify/#handle/#to_response — and, like them, a standalone entrypoint that
136
+ # carries its own InvokedVia wrap rather than relying on #call.
117
137
  def challenge_response(request)
118
- return Response.new(status: 405) unless @challenge
138
+ Axn::Extensions::InvokedVia.with(:webhooks) do
139
+ next Response.new(status: 405) unless @challenge
119
140
 
120
- # The Challenge axn computes the exact Response (200 echo / 403 guard-fail / 400 nil).
121
- # Only a raising resolver/guard makes it not-ok -> a reported 500.
122
- result = Challenge.call(request:, resolver: @challenge[:resolver], guard: @challenge[:guard], vendor: @name)
123
- result.ok? ? result.response : Response.new(status: 500)
141
+ # The Challenge axn computes the exact Response (200 echo / 403 guard-fail / 400 nil).
142
+ # Only a raising resolver/guard makes it not-ok -> a reported 500.
143
+ result = Challenge.call(request:, resolver: @challenge[:resolver], guard: @challenge[:guard], vendor: @name)
144
+ result.ok? ? result.response : Response.new(status: 500)
145
+ end
124
146
  end
125
147
 
126
148
  # The Rack app entry point (spec: mount-first packaging). `Inbound[:vendor]` (this object)
@@ -128,17 +150,19 @@ module Axn
128
150
  # mount owns the whole path and every verb: POST -> #to_response, GET -> #challenge_response,
129
151
  # anything else -> 405. Named `call`, deliberately reserved since Phase 3 (see #handle).
130
152
  def call(env)
131
- built = BuildRequest.call(env:, vendor: @name)
132
- return Response.new(status: 500).to_rack unless built.ok?
133
-
134
- request = built.request
135
- response =
136
- case request.http_method
137
- when "POST" then to_response(request)
138
- when "GET" then challenge_response(request)
139
- else Response.new(status: 405)
140
- end
141
- response.to_rack
153
+ Axn::Extensions::InvokedVia.with(:webhooks) do
154
+ built = BuildRequest.call(env:, vendor: @name)
155
+ next Response.new(status: 500).to_rack unless built.ok?
156
+
157
+ request = built.request
158
+ response =
159
+ case request.http_method
160
+ when "POST" then to_response(request)
161
+ when "GET" then challenge_response(request)
162
+ else Response.new(status: 405)
163
+ end
164
+ response.to_rack
165
+ end
142
166
  end
143
167
 
144
168
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Axn
4
4
  module Webhooks
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: axn-webhooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kali Donovan
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0.pre.alpha.5
18
+ version: 0.1.0.pre.alpha.6
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: 0.2.0
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: 0.1.0.pre.alpha.5
28
+ version: 0.1.0.pre.alpha.6
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: 0.2.0