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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +22 -0
- data/lib/axn/webhooks/inbound/endpoint.rb +64 -40
- data/lib/axn/webhooks/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8b8fbe499b148a37eeebd9a111dd27343dd39618c11ebf00c2d01b0c5a21a30
|
|
4
|
+
data.tar.gz: 99a2eb19546c6f49ec060811cfa83d0b063e0bf1dc77a8156843c4da6197e1fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
|
|
100
|
+
Axn::Extensions::InvokedVia.with(:webhooks) do
|
|
101
|
+
verified = verify(request)
|
|
102
|
+
next verified unless verified.ok? && @dispatch
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
138
|
+
Axn::Extensions::InvokedVia.with(:webhooks) do
|
|
139
|
+
next Response.new(status: 405) unless @challenge
|
|
119
140
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
data/lib/axn/webhooks/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
28
|
+
version: 0.1.0.pre.alpha.6
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: 0.2.0
|