hooks-ruby 0.2.1 → 0.3.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: 47555e043e83129f208e5925c22dbbf12aadba0358a70272461952355251c869
4
- data.tar.gz: b2760979c328b79cf82fe8b5a37a67261f2b5085208488ca928662e1915d9f06
3
+ metadata.gz: 8cd5b56450a97783c263526f9a1a9e3003f1ed587a0da12bfb7a21b428091d2b
4
+ data.tar.gz: 67954e277623e99daafc8b3d86c9bdfa62b871319f4bff759789cd32048e752f
5
5
  SHA512:
6
- metadata.gz: 90b666eb68b986092e56e6db8140af3e6aae5b03a59e0e546beeee4a314a97ed75aaf4122687087cc58b258d88462a6fc5829339f56510e9d123f9b0d7c414ed
7
- data.tar.gz: e50173bf684c3b458fe489667b05a65737658160c5a01603f7b8061419945fe41b07d04e61861524c8af5b445e2d63aa78b90ac1a3a07bb2fed481c50cc04406
6
+ metadata.gz: 41cbc5a653f452e9c6da4ffd526a1b7ea37532712ae820223ccbdb93809789e949d4370fe5ba18036a0a08b8446280ff44471088703c296f769d8c83b6003cfa
7
+ data.tar.gz: 99619a010a1fca90a1bda4f78850c66f705390be9677a1006cfdf72904e9ae0443bd1c0fed02dbbe014515c4df707a84b305460e111eb086fca76af3dec1ad61
data/README.md CHANGED
@@ -50,9 +50,11 @@ Here is a very high-level overview of how Hooks works:
50
50
  ```yaml
51
51
  # file: config/endpoints/hello.yml
52
52
  path: /hello
53
- handler: MyCustomHandler # This is a custom handler plugin you would define in the plugins/handlers directory
53
+ handler: my_custom_handler # This is a custom handler plugin you would define in the plugins/handlers directory (snake_case)
54
54
  ```
55
55
 
56
+ > Note: If your handler's class name is `MyCustomHandler`, you would define it in the `plugins/handlers/my_custom_handler.rb` file. The `handler` field in the endpoint configuration file should be the snake_case version of the class name. So if your handler class is `MyCustomHandler`, you would use `my_custom_handler` in the endpoint configuration file.
57
+
56
58
  3. Now create a corresponding handler plugin in the `plugins/handlers` directory. Here is an example of a simple handler plugin:
57
59
 
58
60
  ```ruby
@@ -64,7 +66,7 @@ Here is a very high-level overview of how Hooks works:
64
66
  # For this example, we will just return a success message
65
67
  {
66
68
  status: "success",
67
- handler: "MyCustomHandler",
69
+ handler: "my_custom_handler",
68
70
  payload_received: payload,
69
71
  timestamp: Time.now.utc.iso8601
70
72
  }
@@ -208,16 +210,16 @@ Endpoint configurations are defined in the `config/endpoints` directory. Each en
208
210
  ```yaml
209
211
  # file: config/endpoints/hello.yml
210
212
  path: /hello # becomes /webhooks/hello based on the root_path in hooks.yml
211
- handler: HelloHandler # This is a custom handler plugin you would define in the plugins/handlers
213
+ handler: hello_handler # This is a custom handler plugin you would define in the plugins/handlers
212
214
  ```
213
215
 
214
216
  ```yaml
215
217
  # file: config/endpoints/goodbye.yml
216
218
  path: /goodbye # becomes /webhooks/goodbye based on the root_path in hooks.yml
217
- handler: GoodbyeHandler # This is another custom handler plugin you would define in the plugins/handlers
219
+ handler: goodbye_handler # This is another custom handler plugin you would define in the plugins/handlers
218
220
 
219
221
  auth:
220
- type: Goodbye # This is a custom authentication plugin you would define in the plugins/auth
222
+ type: goodbye # This is a custom authentication plugin you would define in the plugins/auth
221
223
  secret_env_key: GOODBYE_API_KEY # the name of the environment variable containing the secret
222
224
  header: Authorization
223
225
 
@@ -255,7 +257,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
255
257
  # Ditto for the goodbye endpoint
256
258
  {
257
259
  message: "goodbye webhook processed successfully",
258
- handler: "GoodbyeHandler",
260
+ handler: "goodbye_handler",
259
261
  timestamp: Time.now.utc.iso8601
260
262
  }
261
263
  end
data/lib/hooks/app/api.rb CHANGED
@@ -4,6 +4,7 @@ require "grape"
4
4
  require "json"
5
5
  require "securerandom"
6
6
  require_relative "helpers"
7
+ require_relative "../core/network/ip_filtering"
7
8
  require_relative "auth/auth"
8
9
  require_relative "rack_env_builder"
9
10
  require_relative "../plugins/handlers/base"
@@ -82,6 +83,13 @@ module Hooks
82
83
  plugin.on_request(rack_env)
83
84
  end
84
85
 
86
+ # IP filtering before processing the request if defined
87
+ # If IP filtering is enabled at either global or endpoint level, run the filtering rules
88
+ # before processing the request
89
+ if config[:ip_filtering] || endpoint_config[:ip_filtering]
90
+ ip_filtering!(headers, endpoint_config, config, request_context, rack_env)
91
+ end
92
+
85
93
  enforce_request_limits(config, request_context)
86
94
  request.body.rewind
87
95
  raw_body = request.body.read
@@ -3,6 +3,7 @@
3
3
  require "securerandom"
4
4
  require_relative "../security"
5
5
  require_relative "../core/plugin_loader"
6
+ require_relative "../core/network/ip_filtering"
6
7
 
7
8
  module Hooks
8
9
  module App
@@ -74,7 +75,7 @@ module Hooks
74
75
 
75
76
  # Load handler class
76
77
  #
77
- # @param handler_class_name [String] The name of the handler class to load
78
+ # @param handler_class_name [String] The name of the handler in snake_case (e.g., "github_handler")
78
79
  # @return [Object] An instance of the loaded handler class
79
80
  # @raise [StandardError] If handler cannot be found
80
81
  def load_handler(handler_class_name)
@@ -88,6 +89,28 @@ module Hooks
88
89
  return handler_class.new
89
90
  end
90
91
 
92
+ # Verifies the incoming request passes the configured IP filtering rules.
93
+ #
94
+ # This method assumes that the client IP address is available in the request headers (e.g., `X-Forwarded-For`).
95
+ # The headers that is used is configurable via the endpoint configuration.
96
+ # It checks the IP address against the allowed and denied lists defined in the endpoint configuration.
97
+ # If the IP address is not allowed, it instantly returns an error response via the `error!` method.
98
+ # If the IP filtering configuration is missing or invalid, it raises an error.
99
+ # If IP filtering is configured at the global level, it will also check against the global configuration first,
100
+ # and then against the endpoint-specific configuration.
101
+ #
102
+ # @param headers [Hash] The request headers.
103
+ # @param endpoint_config [Hash] The endpoint configuration, must include :ip_filtering key.
104
+ # @param global_config [Hash] The global configuration (optional, for compatibility).
105
+ # @param request_context [Hash] Context for the request, e.g. request ID, path, handler (optional).
106
+ # @param env [Hash] The Rack environment
107
+ # @raise [StandardError] Raises error if IP filtering fails or is misconfigured.
108
+ # @return [void]
109
+ # @note This method will halt execution with an error if IP filtering rules fail.
110
+ def ip_filtering!(headers, endpoint_config, global_config, request_context, env)
111
+ Hooks::Core::Network::IpFiltering.ip_filtering!(headers, endpoint_config, global_config, request_context, env)
112
+ end
113
+
91
114
  private
92
115
 
93
116
  # Safely parse JSON
@@ -72,6 +72,8 @@ module Hooks
72
72
  end
73
73
 
74
74
  # Add HTTP headers to the environment with proper Rack naming convention
75
+ # Note: This will generally add headers like HTTP_X_CUSTOM_HEADER. For example, the HTTP_X_FORWARDED_FOR
76
+ # is a common header that is used to pass the original client IP address through proxies.
75
77
  #
76
78
  # @param rack_env [Hash] Environment hash to modify
77
79
  def add_http_headers(rack_env)
@@ -27,6 +27,12 @@ module Hooks
27
27
  optional(:endpoints_dir).filled(:string)
28
28
  optional(:use_catchall_route).filled(:bool)
29
29
  optional(:normalize_headers).filled(:bool)
30
+
31
+ optional(:ip_filtering).hash do
32
+ optional(:ip_header).filled(:string)
33
+ optional(:allowlist).array(:string)
34
+ optional(:blocklist).array(:string)
35
+ end
30
36
  end
31
37
 
32
38
  # Endpoint configuration schema
@@ -52,6 +58,12 @@ module Hooks
52
58
  optional(:key_value_separator).filled(:string)
53
59
  end
54
60
 
61
+ optional(:ip_filtering).hash do
62
+ optional(:ip_header).filled(:string)
63
+ optional(:allowlist).array(:string)
64
+ optional(:blocklist).array(:string)
65
+ end
66
+
55
67
  optional(:opts).hash
56
68
  end
57
69
 
@@ -125,11 +137,12 @@ module Hooks
125
137
  # Must not be empty or only whitespace
126
138
  return false if handler_name.strip.empty?
127
139
 
128
- # Must match a safe pattern: alphanumeric + underscore, starting with uppercase
129
- return false unless handler_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
140
+ # Must match strict snake_case pattern: starts with lowercase, no trailing/consecutive underscores
141
+ return false unless handler_name.match?(/\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/)
130
142
 
131
- # Must not be a system/built-in class name
132
- return false if Hooks::Security::DANGEROUS_CLASSES.include?(handler_name)
143
+ # Convert to PascalCase for security check (since DANGEROUS_CLASSES uses PascalCase)
144
+ pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
145
+ return false if Hooks::Security::DANGEROUS_CLASSES.include?(pascal_case_name)
133
146
 
134
147
  true
135
148
  end
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "../../plugins/handlers/error"
5
+
6
+ module Hooks
7
+ module Core
8
+ module Network
9
+ # Application-level IP filtering functionality for HTTP requests.
10
+ #
11
+ # This class provides robust IP filtering capabilities supporting both allowlist
12
+ # and blocklist filtering with CIDR notation support. It can extract client IP
13
+ # addresses from various HTTP headers and validate them against configured rules.
14
+ #
15
+ # The filtering logic follows these rules:
16
+ # 1. If a blocklist is configured and the IP matches, access is denied
17
+ # 2. If an allowlist is configured, the IP must match to be allowed
18
+ # 3. If no allowlist is configured and IP is not blocked, access is allowed
19
+ #
20
+ # @example Basic usage with endpoint configuration
21
+ # config = {
22
+ # ip_filtering: {
23
+ # allowlist: ["192.168.1.0/24", "10.0.0.1"],
24
+ # blocklist: ["192.168.1.100"],
25
+ # ip_header: "X-Real-IP"
26
+ # }
27
+ # }
28
+ # IpFiltering.ip_filtering!(headers, config, {}, {}, env)
29
+ #
30
+ # @note This class is designed to work with Rack-based applications and
31
+ # expects headers to be in a Hash format.
32
+ class IpFiltering
33
+ # Default HTTP header to check for client IP address.
34
+ # @return [String] the default header name
35
+ DEFAULT_IP_HEADER = "X-Forwarded-For"
36
+
37
+ # Verifies that an incoming request passes the configured IP filtering rules.
38
+ #
39
+ # This method extracts the client IP address from request headers and validates
40
+ # it against configured allowlist and blocklist rules. The method will halt
41
+ # execution by raising an error if the IP filtering rules fail.
42
+ #
43
+ # The IP filtering configuration can be defined at both global and endpoint levels,
44
+ # with endpoint configuration taking precedence. If no IP filtering is configured,
45
+ # the method returns early without performing any checks.
46
+ #
47
+ # The client IP is extracted from HTTP headers, with support for configurable
48
+ # header names. The default header is X-Forwarded-For, which can contain multiple
49
+ # comma-separated IPs (the first IP is used as the original client).
50
+ #
51
+ # @param headers [Hash] The request headers as key-value pairs
52
+ # @param endpoint_config [Hash] The endpoint-specific configuration containing :ip_filtering
53
+ # @param global_config [Hash] The global configuration (optional, for compatibility)
54
+ # @param request_context [Hash] Context information for the request (e.g., request_id, path, handler)
55
+ # @param env [Hash] The Rack environment hash
56
+ #
57
+ # @raise [Hooks::Plugins::Handlers::Error] Raises a 403 error if IP filtering rules fail
58
+ # @return [void] Returns nothing if IP filtering passes or is not configured
59
+ #
60
+ # @example Successful IP filtering
61
+ # headers = { "X-Forwarded-For" => "192.168.1.50" }
62
+ # config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
63
+ # IpFiltering.ip_filtering!(headers, config, {}, { request_id: "123" }, env)
64
+ #
65
+ # @example IP filtering failure
66
+ # headers = { "X-Forwarded-For" => "10.0.0.1" }
67
+ # config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
68
+ # # Raises Hooks::Plugins::Handlers::Error with 403 status
69
+ # IpFiltering.ip_filtering!(headers, config, {}, { request_id: "123" }, env)
70
+ #
71
+ # @note This method assumes that the client IP address is available in the request headers
72
+ # @note If the IP filtering configuration is missing or invalid, it raises an error
73
+ # @note This method will halt execution with an error if IP filtering rules fail
74
+ def self.ip_filtering!(headers, endpoint_config, global_config, request_context, env)
75
+ # Determine which IP filtering configuration to use
76
+ ip_config = resolve_ip_config(endpoint_config, global_config)
77
+ return unless ip_config # No IP filtering configured
78
+
79
+ # Extract client IP from headers
80
+ client_ip = extract_client_ip(headers, ip_config)
81
+ return unless client_ip # No client IP found
82
+
83
+ # Validate IP against filtering rules
84
+ unless ip_allowed?(client_ip, ip_config)
85
+ request_id = request_context&.dig(:request_id) || request_context&.dig("request_id")
86
+ error_msg = {
87
+ error: "ip_filtering_failed",
88
+ message: "IP address not allowed",
89
+ request_id: request_id
90
+ }
91
+ raise Hooks::Plugins::Handlers::Error.new(error_msg, 403)
92
+ end
93
+ end
94
+
95
+ # Resolves the IP filtering configuration to use for the current request.
96
+ #
97
+ # This method determines which IP filtering configuration should be applied
98
+ # by checking endpoint-specific configuration first, then falling back to
99
+ # global configuration. This allows for flexible configuration inheritance
100
+ # with endpoint-level overrides.
101
+ #
102
+ # @param endpoint_config [Hash] The endpoint-specific configuration
103
+ # @param global_config [Hash] The global application configuration
104
+ #
105
+ # @return [Hash, nil] The IP filtering configuration hash, or nil if none configured
106
+ #
107
+ # @example With endpoint configuration
108
+ # endpoint_config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
109
+ # global_config = { ip_filtering: { allowlist: ["10.0.0.0/8"] } }
110
+ # resolve_ip_config(endpoint_config, global_config)
111
+ # # => { allowlist: ["192.168.1.0/24"] }
112
+ #
113
+ # @example With only global configuration
114
+ # endpoint_config = {}
115
+ # global_config = { ip_filtering: { allowlist: ["10.0.0.0/8"] } }
116
+ # resolve_ip_config(endpoint_config, global_config)
117
+ # # => { allowlist: ["10.0.0.0/8"] }
118
+ #
119
+ # @note Endpoint-level configuration takes precedence over global configuration
120
+ private_class_method def self.resolve_ip_config(endpoint_config, global_config)
121
+ # Endpoint-level configuration takes precedence over global configuration
122
+ endpoint_config[:ip_filtering] || global_config[:ip_filtering]
123
+ end
124
+
125
+ # Extracts the client IP address from request headers.
126
+ #
127
+ # This method looks for the client IP in the specified header (or default
128
+ # X-Forwarded-For header). It performs case-insensitive header matching
129
+ # and handles comma-separated IP lists by taking the first IP address,
130
+ # which represents the original client in proxy chains.
131
+ #
132
+ # @param headers [Hash] The request headers as key-value pairs
133
+ # @param ip_config [Hash] The IP filtering configuration containing :ip_header
134
+ #
135
+ # @return [String, nil] The client IP address, or nil if not found or empty
136
+ #
137
+ # @example Extracting from X-Forwarded-For
138
+ # headers = { "X-Forwarded-For" => "192.168.1.50, 10.0.0.1" }
139
+ # ip_config = { ip_header: "X-Forwarded-For" }
140
+ # extract_client_ip(headers, ip_config)
141
+ # # => "192.168.1.50"
142
+ #
143
+ # @example Extracting from custom header
144
+ # headers = { "X-Real-IP" => "203.0.113.45" }
145
+ # ip_config = { ip_header: "X-Real-IP" }
146
+ # extract_client_ip(headers, ip_config)
147
+ # # => "203.0.113.45"
148
+ #
149
+ # @note Case-insensitive header lookup is performed
150
+ # @note For comma-separated IP lists, only the first IP is returned
151
+ private_class_method def self.extract_client_ip(headers, ip_config)
152
+ # Use configured header or default to X-Forwarded-For
153
+ ip_header = ip_config[:ip_header] || DEFAULT_IP_HEADER
154
+
155
+ # Case-insensitive header lookup
156
+ headers.each do |key, value|
157
+ if key.to_s.downcase == ip_header.downcase
158
+ # X-Forwarded-For can contain multiple IPs, take the first one (original client)
159
+ client_ip = value.to_s.split(",").first&.strip
160
+ return client_ip unless client_ip.nil? || client_ip.empty?
161
+ end
162
+ end
163
+
164
+ nil
165
+ end
166
+
167
+ # Determines if a client IP address is allowed based on filtering rules.
168
+ #
169
+ # This method implements the core IP filtering logic by checking the client
170
+ # IP against configured blocklist and allowlist rules. The filtering follows
171
+ # these precedence rules:
172
+ # 1. If blocklist exists and IP matches, deny access (return false)
173
+ # 2. If allowlist exists, IP must match to be allowed (return true/false)
174
+ # 3. If no allowlist exists and IP not blocked, allow access (return true)
175
+ #
176
+ # @param client_ip [String] The client IP address to validate
177
+ # @param ip_config [Hash] The IP filtering configuration containing :blocklist and/or :allowlist
178
+ #
179
+ # @return [Boolean] true if IP is allowed, false if blocked or invalid
180
+ #
181
+ # @example IP allowed by allowlist
182
+ # client_ip = "192.168.1.50"
183
+ # ip_config = { allowlist: ["192.168.1.0/24"] }
184
+ # ip_allowed?(client_ip, ip_config)
185
+ # # => true
186
+ #
187
+ # @example IP blocked by blocklist
188
+ # client_ip = "192.168.1.100"
189
+ # ip_config = { blocklist: ["192.168.1.100"] }
190
+ # ip_allowed?(client_ip, ip_config)
191
+ # # => false
192
+ #
193
+ # @example Invalid IP format
194
+ # client_ip = "invalid-ip"
195
+ # ip_config = { allowlist: ["192.168.1.0/24"] }
196
+ # ip_allowed?(client_ip, ip_config)
197
+ # # => false
198
+ #
199
+ # @note Invalid IP addresses are automatically denied
200
+ # @note Blocklist rules take precedence over allowlist rules
201
+ private_class_method def self.ip_allowed?(client_ip, ip_config)
202
+ # Parse client IP
203
+ begin
204
+ client_addr = IPAddr.new(client_ip)
205
+ rescue IPAddr::InvalidAddressError
206
+ return false # Invalid IP format
207
+ end
208
+
209
+ # Check blocklist first (if IP is blocked, deny immediately)
210
+ if ip_config[:blocklist]&.any?
211
+ return false if ip_matches_list?(client_addr, ip_config[:blocklist])
212
+ end
213
+
214
+ # Check allowlist (if defined, IP must be in allowlist)
215
+ if ip_config[:allowlist]&.any?
216
+ return ip_matches_list?(client_addr, ip_config[:allowlist])
217
+ end
218
+
219
+ # If no allowlist is defined and IP is not in blocklist, allow
220
+ true
221
+ end
222
+
223
+ # Checks if a client IP address matches any pattern in an IP list.
224
+ #
225
+ # This method iterates through a list of IP patterns (which can include
226
+ # individual IPs or CIDR ranges) and determines if the client IP matches
227
+ # any of them. It uses Ruby's IPAddr class for robust IP address and
228
+ # CIDR range matching, with error handling for invalid IP patterns.
229
+ #
230
+ # @param client_addr [IPAddr] The client IP address as an IPAddr object
231
+ # @param ip_list [Array<String>] Array of IP patterns (IPs or CIDR ranges)
232
+ #
233
+ # @return [Boolean] true if client IP matches any pattern in the list, false otherwise
234
+ #
235
+ # @example Matching individual IP
236
+ # client_addr = IPAddr.new("192.168.1.50")
237
+ # ip_list = ["192.168.1.50", "10.0.0.1"]
238
+ # ip_matches_list?(client_addr, ip_list)
239
+ # # => true
240
+ #
241
+ # @example Matching CIDR range
242
+ # client_addr = IPAddr.new("192.168.1.50")
243
+ # ip_list = ["192.168.1.0/24", "10.0.0.0/8"]
244
+ # ip_matches_list?(client_addr, ip_list)
245
+ # # => true
246
+ #
247
+ # @example No match found
248
+ # client_addr = IPAddr.new("203.0.113.45")
249
+ # ip_list = ["192.168.1.0/24", "10.0.0.0/8"]
250
+ # ip_matches_list?(client_addr, ip_list)
251
+ # # => false
252
+ #
253
+ # @note Invalid IP patterns in the list are silently skipped
254
+ # @note Supports both IPv4 and IPv6 addresses and ranges
255
+ private_class_method def self.ip_matches_list?(client_addr, ip_list)
256
+ ip_list.each do |ip_pattern|
257
+ begin
258
+ pattern_addr = IPAddr.new(ip_pattern.to_s)
259
+ return true if pattern_addr.include?(client_addr)
260
+ rescue IPAddr::InvalidAddressError
261
+ # Skip invalid IP patterns
262
+ next
263
+ end
264
+ end
265
+ false
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -61,11 +61,13 @@ module Hooks
61
61
 
62
62
  # Get handler plugin class by name
63
63
  #
64
- # @param handler_name [String] Name of the handler (e.g., "DefaultHandler", "Team1Handler")
64
+ # @param handler_name [String] Name of the handler in snake_case (e.g., "github_handler", "team_1_handler")
65
65
  # @return [Class] The handler plugin class
66
66
  # @raise [StandardError] if handler not found
67
67
  def get_handler_plugin(handler_name)
68
- plugin_class = @handler_plugins[handler_name]
68
+ # Convert snake_case to PascalCase for registry lookup
69
+ pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
70
+ plugin_class = @handler_plugins[pascal_case_name]
69
71
 
70
72
  unless plugin_class
71
73
  raise StandardError, "Handler plugin '#{handler_name}' not found. Available handlers: #{@handler_plugins.keys.join(', ')}"
@@ -4,6 +4,7 @@ require "rack/utils"
4
4
  require_relative "../../core/log"
5
5
  require_relative "../../core/global_components"
6
6
  require_relative "../../core/component_access"
7
+ require_relative "timestamp_validator"
7
8
 
8
9
  module Hooks
9
10
  module Plugins
@@ -53,6 +54,13 @@ module Hooks
53
54
  return secret.strip
54
55
  end
55
56
 
57
+ # Get timestamp validator instance
58
+ #
59
+ # @return [TimestampValidator] Singleton timestamp validator instance
60
+ def self.timestamp_validator
61
+ TimestampValidator.new
62
+ end
63
+
56
64
  # Find a header value by name with case-insensitive matching
57
65
  #
58
66
  # @param headers [Hash] HTTP headers from the request
@@ -3,7 +3,6 @@
3
3
  require "openssl"
4
4
  require "time"
5
5
  require_relative "base"
6
- require_relative "timestamp_validator"
7
6
 
8
7
  module Hooks
9
8
  module Plugins
@@ -271,14 +270,6 @@ module Hooks
271
270
  timestamp_validator.valid?(timestamp_value, tolerance)
272
271
  end
273
272
 
274
- # Get timestamp validator instance
275
- #
276
- # @return [TimestampValidator] Singleton timestamp validator instance
277
- # @api private
278
- def self.timestamp_validator
279
- @timestamp_validator ||= TimestampValidator.new
280
- end
281
-
282
273
  # Compute HMAC signature based on configuration requirements
283
274
  #
284
275
  # Generates the expected HMAC signature for the given payload using the
data/lib/hooks/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  module Hooks
5
5
  # Current version of the Hooks webhook framework
6
6
  # @return [String] The version string following semantic versioning
7
- VERSION = "0.2.1".freeze
7
+ VERSION = "0.3.1".freeze
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hooks-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - github
@@ -138,6 +138,7 @@ files:
138
138
  - lib/hooks/core/global_components.rb
139
139
  - lib/hooks/core/log.rb
140
140
  - lib/hooks/core/logger_factory.rb
141
+ - lib/hooks/core/network/ip_filtering.rb
141
142
  - lib/hooks/core/plugin_loader.rb
142
143
  - lib/hooks/core/stats.rb
143
144
  - lib/hooks/plugins/auth/base.rb