philiprehberger-webhook_builder 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f8ace17a470352fbec4b6af92b0e625b346bc6c0580c9e7a53c8ff8ea2c30260
4
+ data.tar.gz: c279d283cb23ac437bd9496b21788de77defe91b68027ae462d47197dd27b0b7
5
+ SHA512:
6
+ metadata.gz: 8fcc581ae60c693c6e5ddaa623fc446e80cb237cad545ae13e142c629903624395c9d189a8228056e63193c15e838c2725137914538bc9e8b1c876e9f36a0b28
7
+ data.tar.gz: ddddb6c9797f16e38dd7797fb4eee25bc265ea903816772b86b92e5a9fca52d50fbbf362d5491ace4368fd2b5152bae635caaeaf693f61ed18ae072408c33f12
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-22
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Webhook client with configurable URL, secret, timeout, and max retries
15
+ - HMAC-SHA256 payload signing
16
+ - Automatic retry with exponential backoff on failure
17
+ - Delivery tracking with success status, response code, attempts, and duration
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # philiprehberger-webhook_builder
2
+
3
+ [![Tests](https://github.com/philiprehberger/rb-webhook-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-webhook-builder/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-webhook_builder.svg)](https://rubygems.org/gems/philiprehberger-webhook_builder)
5
+ [![License](https://img.shields.io/github/license/philiprehberger/rb-webhook-builder)](LICENSE)
6
+
7
+ Webhook delivery client with HMAC signing, retry, and tracking
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.1
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "philiprehberger-webhook_builder"
19
+ ```
20
+
21
+ Or install directly:
22
+
23
+ ```bash
24
+ gem install philiprehberger-webhook_builder
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require "philiprehberger/webhook_builder"
31
+
32
+ client = Philiprehberger::WebhookBuilder.new(
33
+ url: "https://example.com/webhooks",
34
+ secret: "your-signing-secret"
35
+ )
36
+
37
+ delivery = client.deliver(event: "order.created", payload: { id: 123, total: 49.99 })
38
+ delivery.success? # => true
39
+ delivery.response_code # => 200
40
+ ```
41
+
42
+ ### Custom Options
43
+
44
+ ```ruby
45
+ client = Philiprehberger::WebhookBuilder.new(
46
+ url: "https://api.example.com/hooks",
47
+ secret: "hmac-secret",
48
+ timeout: 10,
49
+ max_retries: 5
50
+ )
51
+ ```
52
+
53
+ ### Delivery Tracking
54
+
55
+ ```ruby
56
+ delivery = client.deliver(event: "user.updated", payload: { id: 42 })
57
+
58
+ delivery.success? # => true/false
59
+ delivery.response_code # => 200
60
+ delivery.attempts # => 1
61
+ delivery.duration # => 0.342 (seconds)
62
+ delivery.response_body # => '{"ok":true}'
63
+ delivery.error # => nil or error message
64
+ ```
65
+
66
+ ### HMAC Signing
67
+
68
+ Every request includes an `X-Webhook-Signature` header with an HMAC-SHA256 hex digest of the JSON body, signed with the configured secret. The `X-Webhook-Event` header contains the event type.
69
+
70
+ ## API
71
+
72
+ ### `Client`
73
+
74
+ | Method | Description |
75
+ |--------|-------------|
76
+ | `.new(url:, secret:, timeout:, max_retries:)` | Create a webhook client (timeout defaults to 30s, retries to 3) |
77
+ | `#deliver(event:, payload:)` | Deliver a webhook event and return a Delivery |
78
+
79
+ ### `Delivery`
80
+
81
+ | Method | Description |
82
+ |--------|-------------|
83
+ | `#success?` | Whether the delivery succeeded (2xx response) |
84
+ | `#response_code` | The HTTP response code |
85
+ | `#attempts` | Number of delivery attempts made |
86
+ | `#duration` | Total duration in seconds across all attempts |
87
+ | `#response_body` | The response body string |
88
+ | `#error` | Error message if delivery failed |
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ bundle install
94
+ bundle exec rspec
95
+ bundle exec rubocop
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "openssl"
7
+ require "time"
8
+
9
+ module Philiprehberger
10
+ module WebhookBuilder
11
+ # Webhook delivery client with HMAC signing, retry, and tracking.
12
+ class Client
13
+ # @return [String] the webhook endpoint URL
14
+ attr_reader :url
15
+
16
+ # @return [Integer] the HTTP timeout in seconds
17
+ attr_reader :timeout
18
+
19
+ # @return [Integer] the maximum number of delivery attempts
20
+ attr_reader :max_retries
21
+
22
+ # Create a new webhook client.
23
+ #
24
+ # @param url [String] the webhook endpoint URL
25
+ # @param secret [String] the HMAC-SHA256 signing secret
26
+ # @param timeout [Integer] HTTP timeout in seconds (default: 30)
27
+ # @param max_retries [Integer] maximum retry attempts on failure (default: 3)
28
+ def initialize(url:, secret:, timeout: 30, max_retries: 3)
29
+ @url = url
30
+ @secret = secret
31
+ @timeout = timeout
32
+ @max_retries = max_retries
33
+ end
34
+
35
+ # Deliver a webhook event.
36
+ #
37
+ # @param event [String] the event type (e.g., "order.created")
38
+ # @param payload [Hash] the event payload
39
+ # @return [Delivery] the delivery result
40
+ def deliver(event:, payload:)
41
+ body = JSON.generate({ event: event, payload: payload, timestamp: Time.now.utc.iso8601 })
42
+ signature = sign(body)
43
+
44
+ attempts = 0
45
+ start_time = monotonic_now
46
+ last_response_code = nil
47
+ last_response_body = nil
48
+ last_error = nil
49
+
50
+ loop do
51
+ attempts += 1
52
+ begin
53
+ response = send_request(body, signature, event)
54
+ last_response_code = response.code.to_i
55
+ last_response_body = response.body
56
+
57
+ if last_response_code >= 200 && last_response_code < 300
58
+ return Delivery.new(
59
+ success: true,
60
+ response_code: last_response_code,
61
+ attempts: attempts,
62
+ duration: monotonic_now - start_time,
63
+ response_body: last_response_body
64
+ )
65
+ end
66
+
67
+ last_error = "HTTP #{last_response_code}"
68
+ rescue StandardError => e
69
+ last_error = e.message
70
+ end
71
+
72
+ break if attempts > @max_retries
73
+
74
+ sleep(backoff_delay(attempts))
75
+ end
76
+
77
+ Delivery.new(
78
+ success: false,
79
+ response_code: last_response_code,
80
+ attempts: attempts,
81
+ duration: monotonic_now - start_time,
82
+ response_body: last_response_body,
83
+ error: last_error
84
+ )
85
+ end
86
+
87
+ private
88
+
89
+ # Sign the request body with HMAC-SHA256.
90
+ #
91
+ # @param body [String] the JSON body
92
+ # @return [String] the hex-encoded HMAC signature
93
+ def sign(body)
94
+ OpenSSL::HMAC.hexdigest("SHA256", @secret, body)
95
+ end
96
+
97
+ # Send the HTTP POST request.
98
+ #
99
+ # @param body [String] the JSON body
100
+ # @param signature [String] the HMAC signature
101
+ # @param event [String] the event type
102
+ # @return [Net::HTTPResponse]
103
+ def send_request(body, signature, event)
104
+ uri = URI.parse(@url)
105
+ http = Net::HTTP.new(uri.host, uri.port)
106
+ http.use_ssl = uri.scheme == "https"
107
+ http.open_timeout = @timeout
108
+ http.read_timeout = @timeout
109
+
110
+ request = Net::HTTP::Post.new(uri.request_uri)
111
+ request["Content-Type"] = "application/json"
112
+ request["X-Webhook-Signature"] = signature
113
+ request["X-Webhook-Event"] = event
114
+ request["User-Agent"] = "philiprehberger-webhook_builder/#{VERSION}"
115
+ request.body = body
116
+
117
+ http.request(request)
118
+ end
119
+
120
+ # Calculate exponential backoff delay.
121
+ #
122
+ # @param attempt [Integer] the current attempt number
123
+ # @return [Float] delay in seconds
124
+ def backoff_delay(attempt)
125
+ [2**(attempt - 1), 30].min
126
+ end
127
+
128
+ def monotonic_now
129
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module WebhookBuilder
5
+ # Represents the result of a webhook delivery attempt.
6
+ class Delivery
7
+ # @return [Boolean] whether the delivery succeeded (2xx response)
8
+ attr_reader :success
9
+
10
+ # @return [Integer, nil] the HTTP response code
11
+ attr_reader :response_code
12
+
13
+ # @return [Integer] the number of delivery attempts made
14
+ attr_reader :attempts
15
+
16
+ # @return [Float] the total duration in seconds across all attempts
17
+ attr_reader :duration
18
+
19
+ # @return [String, nil] the response body
20
+ attr_reader :response_body
21
+
22
+ # @return [String, nil] the error message if delivery failed
23
+ attr_reader :error
24
+
25
+ # @param success [Boolean]
26
+ # @param response_code [Integer, nil]
27
+ # @param attempts [Integer]
28
+ # @param duration [Float]
29
+ # @param response_body [String, nil]
30
+ # @param error [String, nil]
31
+ def initialize(success:, response_code:, attempts:, duration:, response_body: nil, error: nil)
32
+ @success = success
33
+ @response_code = response_code
34
+ @attempts = attempts
35
+ @duration = duration
36
+ @response_body = response_body
37
+ @error = error
38
+ end
39
+
40
+ # Whether the delivery was successful.
41
+ #
42
+ # @return [Boolean]
43
+ def success?
44
+ @success
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module WebhookBuilder
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module WebhookBuilder
5
+ class Error < StandardError; end
6
+
7
+ # Convenience method to create a new webhook client.
8
+ #
9
+ # @param options [Hash] options passed to {Client#initialize}
10
+ # @return [Client] a new webhook client
11
+ # @see Client#initialize
12
+ def self.new(**options)
13
+ Client.new(**options)
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative "webhook_builder/version"
19
+ require_relative "webhook_builder/delivery"
20
+ require_relative "webhook_builder/client"
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-webhook_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A webhook delivery client that signs payloads with HMAC-SHA256, retries
14
+ failed deliveries with exponential backoff, and tracks delivery status including
15
+ response codes, attempts, and duration.
16
+ email:
17
+ - me@philiprehberger.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE
24
+ - README.md
25
+ - lib/philiprehberger/webhook_builder.rb
26
+ - lib/philiprehberger/webhook_builder/client.rb
27
+ - lib/philiprehberger/webhook_builder/delivery.rb
28
+ - lib/philiprehberger/webhook_builder/version.rb
29
+ homepage: https://github.com/philiprehberger/rb-webhook-builder
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/philiprehberger/rb-webhook-builder
34
+ source_code_uri: https://github.com/philiprehberger/rb-webhook-builder
35
+ changelog_uri: https://github.com/philiprehberger/rb-webhook-builder/blob/main/CHANGELOG.md
36
+ bug_tracker_uri: https://github.com/philiprehberger/rb-webhook-builder/issues
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.1.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.5.22
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Webhook delivery client with HMAC signing, retry, and tracking
57
+ test_files: []