philiprehberger-webhook_builder 0.3.0 → 0.5.0

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: 7754a65fd17d3f711872d837db0a95670d245eab1da96df4d42c723b8d7f9f3d
4
- data.tar.gz: 8be04c1b22974980e572ca5073279d995f6e8f67d8853742de62297e5951a532
3
+ metadata.gz: b3008661457c2c68b820bfb48c133cc513040bb2f5ec35af8e62a4c0318241c9
4
+ data.tar.gz: 461cbff339dfd9fab1bccf97e68c855080ee48f3faa685fc92b2aab81c56aeb2
5
5
  SHA512:
6
- metadata.gz: 5f756201c37028a4f44d5975200d24fb811d87ecbc13467b9a9f3e5c792333c26dc546177871b9bda42d4bef3da1093202aa5f82279622521bf5c443d4fe61f8
7
- data.tar.gz: c09e9a31e51d343140eb0beaa1db83560bd7a35dca976f00332534a888ece0eb8263a9701a6b5bf14720388ad381245b44b089689700fa0298ff9a5b16130742
6
+ metadata.gz: f68594c755f2995c19390964a63ae42ef14832eefca6a0de3c54deb74a6fd1641908bccbfd11e00b7bc4592a303fd95c552fbb7adfe652a94e7a6dac49c724bd
7
+ data.tar.gz: 8f6b0914eab043887af35ea57a03a7f85b2d72d0c7cbc3f8d4eecb7746a31c98ba49c0dbe6983784a07f1cb5e06bfc34094b6e302039ee2af77edf2a2ab1a29b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-05-08
11
+
12
+ ### Added
13
+ - `Backoff::Decorrelated` — AWS-style decorrelated jitter strategy (`delay = base + rand * (min(cap, prev * 3) - base)`); spreads retries to avoid thundering-herd patterns
14
+ - `Client#new(backoff: :decorrelated)` — `:decorrelated` is now a recognized symbol alongside `:exponential`, `:linear`, `:fixed`, and any callable Proc
15
+
16
+ ## [0.4.0] - 2026-04-22
17
+
18
+ ### Added
19
+ - `Client#signature_for(body:)` — compute the HMAC-SHA256 signature this client would send for a given body without performing a delivery.
20
+
10
21
  ## [0.3.0] - 2026-04-18
11
22
 
12
23
  ### Added
data/README.md CHANGED
@@ -86,6 +86,13 @@ client = Philiprehberger::WebhookBuilder.new(
86
86
  backoff: :fixed
87
87
  )
88
88
 
89
+ # Decorrelated jitter (AWS-style): randomized within [base, min(cap, prev*3)]
90
+ client = Philiprehberger::WebhookBuilder.new(
91
+ url: "https://example.com/webhooks",
92
+ secret: "secret",
93
+ backoff: :decorrelated
94
+ )
95
+
89
96
  # Custom Proc backoff
90
97
  client = Philiprehberger::WebhookBuilder.new(
91
98
  url: "https://example.com/webhooks",
@@ -134,6 +141,24 @@ receiver.verify_signature(body: body, signature: signature) # => true
134
141
  receiver.verify_signature(body: body, signature: "tampered") # => false
135
142
  ```
136
143
 
144
+ You can also compute the signature the client would send for a body without
145
+ performing a delivery — useful for preparing payloads offline or mirroring
146
+ `verify_signature`:
147
+
148
+ ```ruby
149
+ require "philiprehberger/webhook_builder"
150
+
151
+ client = Philiprehberger::WebhookBuilder.new(
152
+ url: "https://example.com/webhooks",
153
+ secret: "shared-signing-secret"
154
+ )
155
+
156
+ body = '{"event":"order.created","payload":{"id":1}}'
157
+ signature = client.signature_for(body: body)
158
+
159
+ client.verify_signature(body: body, signature: signature) # => true
160
+ ```
161
+
137
162
  ### Delivery Tracking
138
163
 
139
164
  ```ruby
@@ -164,6 +189,7 @@ delivery.error # => nil or error message
164
189
  | `#deliver(event:, payload:, headers:)` | Deliver a webhook event and return a Delivery |
165
190
  | `#deliver_batch(events)` | Deliver multiple events concurrently and return an array of Delivery results |
166
191
  | `#verify_signature(body:, signature:)` | Constant-time HMAC-SHA256 verification of an incoming signature; returns `true`/`false` and never raises |
192
+ | `#signature_for(body:)` | Compute the HMAC-SHA256 signature for a body without sending |
167
193
 
168
194
  ### `Delivery`
169
195
 
@@ -197,6 +223,13 @@ delivery.error # => nil or error message
197
223
  | `.new(delay:)` | Create fixed strategy (default: delay=1) |
198
224
  | `#call(attempt)` | Returns constant delay |
199
225
 
226
+ ### `Backoff::Decorrelated`
227
+
228
+ | Method | Description |
229
+ |--------|-------------|
230
+ | `.new(base:, max_delay:)` | Create decorrelated jitter strategy (defaults: base=1, max_delay=30) |
231
+ | `#call(attempt)` | Returns randomized delay in `[base, min(max_delay, prev * 3)]` |
232
+
200
233
  ## Development
201
234
 
202
235
  ```bash
@@ -53,6 +53,30 @@ module Philiprehberger
53
53
  end
54
54
  end
55
55
 
56
+ # Decorrelated jitter backoff (AWS-style): each delay is a random value
57
+ # in `[base, [cap, prev * 3].min]`, which spreads retries to avoid
58
+ # thundering-herd effects while still trending toward the cap.
59
+ #
60
+ # See: https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
61
+ class Decorrelated
62
+ # @param base [Numeric] minimum delay in seconds (default: 1)
63
+ # @param max_delay [Numeric] maximum delay in seconds (default: 30)
64
+ def initialize(base: 1, max_delay: 30)
65
+ @base = base.to_f
66
+ @max_delay = max_delay.to_f
67
+ @prev = @base
68
+ end
69
+
70
+ # @param _attempt [Integer] ignored (state is carried in @prev)
71
+ # @return [Float] delay in seconds
72
+ def call(_attempt)
73
+ upper = [@max_delay, @prev * 3].min
74
+ upper = @base if upper < @base
75
+ @prev = @base + (rand * (upper - @base))
76
+ @prev
77
+ end
78
+ end
79
+
56
80
  # Resolve a backoff option into a callable strategy.
57
81
  #
58
82
  # @param option [Symbol, Proc, nil] the backoff strategy
@@ -65,10 +89,14 @@ module Philiprehberger
65
89
  Linear.new
66
90
  when :fixed
67
91
  Fixed.new
92
+ when :decorrelated
93
+ Decorrelated.new
68
94
  when Proc
69
95
  option
70
96
  else
71
- raise ArgumentError, "Unknown backoff strategy: #{option.inspect}. Use :exponential, :linear, :fixed, or a Proc."
97
+ raise ArgumentError,
98
+ "Unknown backoff strategy: #{option.inspect}. " \
99
+ 'Use :exponential, :linear, :fixed, :decorrelated, or a Proc.'
72
100
  end
73
101
  end
74
102
  end
@@ -153,6 +153,18 @@ module Philiprehberger
153
153
  OpenSSL.fixed_length_secure_compare(expected, signature)
154
154
  end
155
155
 
156
+ # Compute the HMAC-SHA256 hex signature this client would send for the given body.
157
+ #
158
+ # Mirrors +verify_signature+: the signature returned by +signature_for(body:)+
159
+ # will verify against the same body. Useful when preparing payloads offline or
160
+ # re-computing signatures without sending a request.
161
+ #
162
+ # @param body [String] the raw request body
163
+ # @return [String] the hex-encoded HMAC signature
164
+ def signature_for(body:)
165
+ sign(body)
166
+ end
167
+
156
168
  private
157
169
 
158
170
  # Sign the request body with HMAC-SHA256.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module WebhookBuilder
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-webhook_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-19 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A webhook delivery client that signs payloads with HMAC-SHA256, retries
14
14
  failed deliveries with configurable backoff strategies, supports batch delivery,