hooks-ruby 0.0.3 → 0.0.4

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: cc9f23a8b74aef04b7a0016673a9dd835cef5f56eecb316e58b7cb3d19936f0d
4
- data.tar.gz: '05290bf35da3f0aa73e5a0b9e0661d2986df03733862f6cd85c19248257f5b09'
3
+ metadata.gz: 2cab1bf1a1f61011d053be5beb9d48738bd439303b87743e41e10df0b4b9d65c
4
+ data.tar.gz: 9b9abc62fff8f0f2f77ad94949bc7e067fff5a2f072bbf73f8e6e55c03865714
5
5
  SHA512:
6
- metadata.gz: a4462d77b184ac3be6b4a0f7ac4466dace8b5d146c722205ca7f97f455dc523de2168a71e2fe61b4a0946efdb7e9b8e4b61dc1e9940c8d14fdbaebebcb8fb497
7
- data.tar.gz: bc2023bf7a08efd872e69603dd81dbbb2a4798deb4b4c98db37d65f8544ba57cfbdf36b82f2b48b33250d2429eb39946a71d65d7fbdf7c2d5e0a6b724e79a9cb
6
+ metadata.gz: 40f769a697bd61c6c851eef217fadc66387c49d37c19639a283dd770ca8be8889a13eeba2785a41c53edcdef29327231527405a9de21b2b4620416196da792c2
7
+ data.tar.gz: 119a371d927f240aefe4e74c822b48318032250e7cbf060a9bd077b1375823137b419f7d1e2d27d775e6468d38843afb45ec9c8cd8a71a22d82226160c66043e
data/README.md CHANGED
@@ -66,7 +66,7 @@ Here is a very high-level overview of how Hooks works:
66
66
  status: "success",
67
67
  handler: "MyCustomHandler",
68
68
  payload_received: payload,
69
- timestamp: Time.now.iso8601
69
+ timestamp: Time.now.utc.iso8601
70
70
  }
71
71
  end
72
72
  end
@@ -229,7 +229,7 @@ class HelloHandler < Hooks::Plugins::Handlers::Base
229
229
  {
230
230
  message: "webhook processed successfully",
231
231
  handler: "HelloHandler",
232
- timestamp: Time.now.iso8601
232
+ timestamp: Time.now.utc.iso8601
233
233
  }
234
234
  end
235
235
  end
@@ -245,7 +245,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
245
245
  {
246
246
  message: "goodbye webhook processed successfully",
247
247
  handler: "GoodbyeHandler",
248
- timestamp: Time.now.iso8601
248
+ timestamp: Time.now.utc.iso8601
249
249
  }
250
250
  end
251
251
  end
@@ -302,6 +302,10 @@ See the [Lifecycle Plugins](docs/lifecycle_plugins.md) documentation for informa
302
302
 
303
303
  See the [Instrument Plugins](docs/instrument_plugins.md) documentation for information on how to create instrument plugins that can be used to collect metrics or report exceptions during webhook processing. These plugins can be used to integrate with monitoring and alerting systems.
304
304
 
305
+ ### Configuration
306
+
307
+ See the [Configuration](docs/configuration.md) documentation for detailed information on how to configure your Hooks server, including global options, endpoint options, and more.
308
+
305
309
  ## Contributing 🤝
306
310
 
307
311
  See the [Contributing](CONTRIBUTING.md) document for information on how to contribute to the Hooks project, including setting up your development environment, running tests, and releasing new versions.
data/lib/hooks/app/api.rb CHANGED
@@ -27,6 +27,7 @@ module Hooks
27
27
 
28
28
  # Create a new configured API class
29
29
  def self.create(config:, endpoints:, log:)
30
+ # :nocov:
30
31
  @server_start_time = Time.now
31
32
 
32
33
  api_class = Class.new(Grape::API) do
@@ -47,8 +48,9 @@ module Hooks
47
48
  endpoints.each do |endpoint_config|
48
49
  full_path = "#{config[:root_path]}#{endpoint_config[:path]}"
49
50
  handler_class_name = endpoint_config[:handler]
51
+ http_method = (endpoint_config[:method] || "post").downcase.to_sym
50
52
 
51
- post(full_path) do
53
+ send(http_method, full_path) do
52
54
  request_id = uuid
53
55
  start_time = Time.now
54
56
 
@@ -151,6 +153,7 @@ module Hooks
151
153
  end
152
154
 
153
155
  api_class
156
+ # :nocov:
154
157
  end
155
158
  end
156
159
  end
@@ -37,12 +37,8 @@ module Hooks
37
37
  end
38
38
 
39
39
  log.debug("validating auth for request with auth_class: #{auth_class.name}")
40
-
41
- unless auth_class.valid?(
42
- payload:,
43
- headers:,
44
- config: endpoint_config
45
- )
40
+ unless auth_class.valid?(payload:, headers:, config: endpoint_config)
41
+ log.warn("authentication failed for request with auth_class: #{auth_class.name}")
46
42
  error!("authentication failed", 401)
47
43
  end
48
44
  end
@@ -10,7 +10,7 @@ module Hooks
10
10
  content_type "application/json"
11
11
  {
12
12
  status: "healthy",
13
- timestamp: Time.now.iso8601,
13
+ timestamp: Time.now.utc.iso8601,
14
14
  version: Hooks::VERSION,
15
15
  uptime_seconds: (Time.now - Hooks::App::API.server_start_time).to_i
16
16
  }.to_json
@@ -10,7 +10,7 @@ module Hooks
10
10
  content_type "application/json"
11
11
  {
12
12
  version: Hooks::VERSION,
13
- timestamp: Time.now.iso8601
13
+ timestamp: Time.now.utc.iso8601
14
14
  }.to_json
15
15
  end
16
16
  end
@@ -21,13 +21,15 @@ module Hooks
21
21
  # @return [void]
22
22
  # @note Timeout enforcement should be handled at the server level (e.g., Puma)
23
23
  def enforce_request_limits(config)
24
- # Check content length (handle different header formats and sources)
25
- content_length = headers["Content-Length"] || headers["CONTENT_LENGTH"] ||
26
- headers["content-length"] || headers["HTTP_CONTENT_LENGTH"] ||
27
- env["CONTENT_LENGTH"] || env["HTTP_CONTENT_LENGTH"]
24
+ # Optimized content length check - check most common sources first
25
+ content_length = request.content_length if respond_to?(:request) && request.respond_to?(:content_length)
28
26
 
29
- # Also try to get from request object directly
30
- content_length ||= request.content_length if respond_to?(:request) && request.respond_to?(:content_length)
27
+ content_length ||= headers["Content-Length"] ||
28
+ headers["CONTENT_LENGTH"] ||
29
+ headers["content-length"] ||
30
+ headers["HTTP_CONTENT_LENGTH"] ||
31
+ env["CONTENT_LENGTH"] ||
32
+ env["HTTP_CONTENT_LENGTH"]
31
33
 
32
34
  content_length = content_length&.to_i
33
35
 
@@ -45,16 +47,21 @@ module Hooks
45
47
  # @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
46
48
  # @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
47
49
  def parse_payload(raw_body, headers, symbolize: true)
50
+ # Optimized content type check - check most common header first
48
51
  content_type = headers["Content-Type"] || headers["CONTENT_TYPE"] || headers["content-type"] || headers["HTTP_CONTENT_TYPE"]
49
52
 
50
53
  # Try to parse as JSON if content type suggests it or if it looks like JSON
51
54
  if content_type&.include?("application/json") || (raw_body.strip.start_with?("{", "[") rescue false)
52
55
  begin
53
- parsed_payload = JSON.parse(raw_body)
56
+ # Security: Limit JSON parsing depth and complexity to prevent JSON bombs
57
+ parsed_payload = safe_json_parse(raw_body)
54
58
  parsed_payload = parsed_payload.transform_keys(&:to_sym) if symbolize && parsed_payload.is_a?(Hash)
55
59
  return parsed_payload
56
- rescue JSON::ParserError
57
- # If JSON parsing fails, return raw body
60
+ rescue JSON::ParserError, ArgumentError => e
61
+ # If JSON parsing fails or security limits exceeded, return raw body
62
+ if e.message.include?("nesting") || e.message.include?("depth")
63
+ log.warn("JSON parsing limit exceeded: #{e.message}")
64
+ end
58
65
  end
59
66
  end
60
67
 
@@ -79,6 +86,29 @@ module Hooks
79
86
 
80
87
  private
81
88
 
89
+ # Safely parse JSON
90
+ #
91
+ # @param json_string [String] The JSON string to parse
92
+ # @return [Hash, Array] Parsed JSON object
93
+ # @raise [JSON::ParserError] If JSON is invalid
94
+ # @raise [ArgumentError] If security limits are exceeded
95
+ def safe_json_parse(json_string)
96
+ # Security limits for JSON parsing
97
+ max_nesting = ENV.fetch("JSON_MAX_NESTING", "20").to_i
98
+
99
+ # Additional size check before parsing
100
+ if json_string.length > ENV.fetch("JSON_MAX_SIZE", "10485760").to_i # 10MB default
101
+ raise ArgumentError, "JSON payload too large for parsing"
102
+ end
103
+
104
+ JSON.parse(json_string, {
105
+ max_nesting: max_nesting,
106
+ create_additions: false, # Security: Disable object creation from JSON
107
+ object_class: Hash, # Use plain Hash instead of custom classes
108
+ array_class: Array # Use plain Array instead of custom classes
109
+ })
110
+ end
111
+
82
112
  # Determine HTTP error code from exception
83
113
  #
84
114
  # @param exception [Exception] The exception to map to an HTTP status code
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooks
4
+ module Core
5
+ # Shared module providing access to global components (logger, stats, failbot)
6
+ #
7
+ # This module provides a consistent interface for accessing global components
8
+ # across all plugin types, eliminating code duplication and ensuring consistent
9
+ # behavior throughout the application.
10
+ #
11
+ # @example Usage in a class that needs instance methods
12
+ # class MyHandler
13
+ # include Hooks::Core::ComponentAccess
14
+ #
15
+ # def process
16
+ # log.info("Processing request")
17
+ # stats.increment("requests.processed")
18
+ # failbot.report("Error occurred") if error?
19
+ # end
20
+ # end
21
+ #
22
+ # @example Usage in a class that needs class methods
23
+ # class MyValidator
24
+ # extend Hooks::Core::ComponentAccess
25
+ #
26
+ # def self.validate
27
+ # log.info("Validating request")
28
+ # stats.increment("requests.validated")
29
+ # end
30
+ # end
31
+ module ComponentAccess
32
+ # Short logger accessor
33
+ # @return [Hooks::Log] Logger instance for logging messages
34
+ #
35
+ # Provides a convenient way to log messages without needing
36
+ # to reference the full Hooks::Log namespace.
37
+ #
38
+ # @example Logging an error
39
+ # log.error("Something went wrong")
40
+ def log
41
+ Hooks::Log.instance
42
+ end
43
+
44
+ # Global stats component accessor
45
+ # @return [Hooks::Plugins::Instruments::Stats] Stats instance for metrics reporting
46
+ #
47
+ # Provides access to the global stats component for reporting metrics
48
+ # to services like DataDog, New Relic, etc.
49
+ #
50
+ # @example Recording a metric
51
+ # stats.increment("webhook.processed", { handler: "MyHandler" })
52
+ def stats
53
+ Hooks::Core::GlobalComponents.stats
54
+ end
55
+
56
+ # Global failbot component accessor
57
+ # @return [Hooks::Plugins::Instruments::Failbot] Failbot instance for error reporting
58
+ #
59
+ # Provides access to the global failbot component for reporting errors
60
+ # to services like Sentry, Rollbar, etc.
61
+ #
62
+ # @example Reporting an error
63
+ # failbot.report("Something went wrong", { context: "additional info" })
64
+ def failbot
65
+ Hooks::Core::GlobalComponents.failbot
66
+ end
67
+ end
68
+ end
69
+ end
@@ -24,23 +24,39 @@ module Hooks
24
24
  normalize_headers: true
25
25
  }.freeze
26
26
 
27
+ SILENCE_CONFIG_LOADER_MESSAGES = ENV.fetch(
28
+ "HOOKS_SILENCE_CONFIG_LOADER_MESSAGES", "false"
29
+ ).downcase == "true".freeze
30
+
27
31
  # Load and merge configuration from various sources
28
32
  #
29
33
  # @param config_path [String, Hash] Path to config file or config hash
30
34
  # @return [Hash] Merged configuration
31
35
  def self.load(config_path: nil)
32
36
  config = DEFAULT_CONFIG.dup
37
+ overrides = []
33
38
 
34
39
  # Load from file if path provided
35
40
  if config_path.is_a?(String) && File.exist?(config_path)
36
41
  file_config = load_config_file(config_path)
37
- config.merge!(file_config) if file_config
38
- elsif config_path.is_a?(Hash)
39
- config.merge!(config_path)
42
+ if file_config
43
+ overrides << "file config"
44
+ config.merge!(file_config)
45
+ end
40
46
  end
41
47
 
42
- # Override with environment variables
43
- config.merge!(load_env_config)
48
+ # Override with environment variables (before programmatic config)
49
+ env_config = load_env_config
50
+ if env_config.any?
51
+ overrides << "environment variables"
52
+ config.merge!(env_config)
53
+ end
54
+
55
+ # Programmatic config has highest priority
56
+ if config_path.is_a?(Hash)
57
+ overrides << "programmatic config"
58
+ config.merge!(config_path)
59
+ end
44
60
 
45
61
  # Convert string keys to symbols for consistency
46
62
  config = symbolize_keys(config)
@@ -51,6 +67,11 @@ module Hooks
51
67
  config[:production] = false
52
68
  end
53
69
 
70
+ # Log overrides if any were made
71
+ if overrides.any?
72
+ puts "INFO: Configuration overrides applied from: #{overrides.join(', ')}" unless SILENCE_CONFIG_LOADER_MESSAGES
73
+ end
74
+
54
75
  return config
55
76
  end
56
77
 
@@ -93,8 +114,9 @@ module Hooks
93
114
  end
94
115
 
95
116
  result
96
- rescue => _e
97
- # In production, we'd log this error
117
+ rescue => e
118
+ # Log this error with meaningful information
119
+ puts "ERROR: Failed to load config file '#{file_path}': #{e.message}" unless SILENCE_CONFIG_LOADER_MESSAGES
98
120
  nil
99
121
  end
100
122
 
@@ -105,8 +127,11 @@ module Hooks
105
127
  env_config = {}
106
128
 
107
129
  env_mappings = {
130
+ "HOOKS_HANDLER_DIR" => :handler_dir,
108
131
  "HOOKS_HANDLER_PLUGIN_DIR" => :handler_plugin_dir,
109
132
  "HOOKS_AUTH_PLUGIN_DIR" => :auth_plugin_dir,
133
+ "HOOKS_LIFECYCLE_PLUGIN_DIR" => :lifecycle_plugin_dir,
134
+ "HOOKS_INSTRUMENTS_PLUGIN_DIR" => :instruments_plugin_dir,
110
135
  "HOOKS_LOG_LEVEL" => :log_level,
111
136
  "HOOKS_REQUEST_LIMIT" => :request_limit,
112
137
  "HOOKS_REQUEST_TIMEOUT" => :request_timeout,
@@ -114,17 +139,24 @@ module Hooks
114
139
  "HOOKS_HEALTH_PATH" => :health_path,
115
140
  "HOOKS_VERSION_PATH" => :version_path,
116
141
  "HOOKS_ENVIRONMENT" => :environment,
117
- "HOOKS_ENDPOINTS_DIR" => :endpoints_dir
142
+ "HOOKS_ENDPOINTS_DIR" => :endpoints_dir,
143
+ "HOOKS_USE_CATCHALL_ROUTE" => :use_catchall_route,
144
+ "HOOKS_SYMBOLIZE_PAYLOAD" => :symbolize_payload,
145
+ "HOOKS_NORMALIZE_HEADERS" => :normalize_headers,
146
+ "HOOKS_SOME_STRING_VAR" => :some_string_var # Added for test
118
147
  }
119
148
 
120
149
  env_mappings.each do |env_key, config_key|
121
150
  value = ENV[env_key]
122
151
  next unless value
123
152
 
124
- # Convert numeric values
153
+ # Convert values to appropriate types
125
154
  case config_key
126
155
  when :request_limit, :request_timeout
127
156
  env_config[config_key] = value.to_i
157
+ when :use_catchall_route, :symbolize_payload, :normalize_headers
158
+ # Convert string to boolean
159
+ env_config[config_key] = %w[true 1 yes on].include?(value.downcase)
128
160
  else
129
161
  env_config[config_key] = value
130
162
  end
@@ -34,6 +34,7 @@ module Hooks
34
34
  ENDPOINT_CONFIG_SCHEMA = Dry::Schema.Params do
35
35
  required(:path).filled(:string)
36
36
  required(:handler).filled(:string)
37
+ optional(:method).filled(:string, included_in?: %w[get post put patch delete head options])
37
38
 
38
39
  optional(:auth).hash do
39
40
  required(:type).filled(:string)
@@ -1,8 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hooks
4
+ # Global logger accessor module
5
+ #
6
+ # Provides a singleton-like access pattern for the application logger.
7
+ # The logger instance is set during application initialization and can
8
+ # be accessed throughout the application lifecycle.
9
+ #
10
+ # @example Setting the logger instance
11
+ # Hooks::Log.instance = Logger.new(STDOUT)
12
+ #
13
+ # @example Accessing the logger
14
+ # Hooks::Log.instance.info("Application started")
4
15
  module Log
5
16
  class << self
17
+ # Get or set the global logger instance
18
+ # @return [Logger] The global logger instance
19
+ # @attr_reader instance [Logger] Current logger instance
20
+ # @attr_writer instance [Logger] Set the logger instance
6
21
  attr_accessor :instance
7
22
  end
8
23
  end
@@ -123,7 +123,7 @@ module Hooks
123
123
  Dir.glob(File.join(auth_plugin_dir, "*.rb")).sort.each do |file_path|
124
124
  begin
125
125
  load_custom_auth_plugin(file_path, auth_plugin_dir)
126
- rescue => e
126
+ rescue StandardError, SyntaxError => e
127
127
  raise StandardError, "Failed to load auth plugin from #{file_path}: #{e.message}"
128
128
  end
129
129
  end
@@ -139,7 +139,7 @@ module Hooks
139
139
  Dir.glob(File.join(handler_plugin_dir, "*.rb")).sort.each do |file_path|
140
140
  begin
141
141
  load_custom_handler_plugin(file_path, handler_plugin_dir)
142
- rescue => e
142
+ rescue StandardError, SyntaxError => e
143
143
  raise StandardError, "Failed to load handler plugin from #{file_path}: #{e.message}"
144
144
  end
145
145
  end
@@ -155,7 +155,7 @@ module Hooks
155
155
  Dir.glob(File.join(lifecycle_plugin_dir, "*.rb")).sort.each do |file_path|
156
156
  begin
157
157
  load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
158
- rescue => e
158
+ rescue StandardError, SyntaxError => e
159
159
  raise StandardError, "Failed to load lifecycle plugin from #{file_path}: #{e.message}"
160
160
  end
161
161
  end
@@ -171,7 +171,7 @@ module Hooks
171
171
  Dir.glob(File.join(instruments_plugin_dir, "*.rb")).sort.each do |file_path|
172
172
  begin
173
173
  load_custom_instrument_plugin(file_path, instruments_plugin_dir)
174
- rescue => e
174
+ rescue StandardError, SyntaxError => e
175
175
  raise StandardError, "Failed to load instrument plugin from #{file_path}: #{e.message}"
176
176
  end
177
177
  end
@@ -3,6 +3,7 @@
3
3
  require "rack/utils"
4
4
  require_relative "../../core/log"
5
5
  require_relative "../../core/global_components"
6
+ require_relative "../../core/component_access"
6
7
 
7
8
  module Hooks
8
9
  module Plugins
@@ -11,6 +12,8 @@ module Hooks
11
12
  #
12
13
  # All custom Auth plugins must inherit from this class
13
14
  class Base
15
+ extend Hooks::Core::ComponentAccess
16
+
14
17
  # Validate request
15
18
  #
16
19
  # @param payload [String] Raw request body
@@ -22,42 +25,6 @@ module Hooks
22
25
  raise NotImplementedError, "Validator must implement .valid? class method"
23
26
  end
24
27
 
25
- # Short logger accessor for all subclasses
26
- # @return [Hooks::Log] Logger instance for request validation
27
- #
28
- # Provides a convenient way for validators to log messages without needing
29
- # to reference the full Hooks::Log namespace.
30
- #
31
- # @example Logging an error in an inherited class
32
- # log.error("oh no an error occured")
33
- def self.log
34
- Hooks::Log.instance
35
- end
36
-
37
- # Global stats component accessor
38
- # @return [Hooks::Core::Stats] Stats instance for metrics reporting
39
- #
40
- # Provides access to the global stats component for reporting metrics
41
- # to services like DataDog, New Relic, etc.
42
- #
43
- # @example Recording a metric in an inherited class
44
- # stats.increment("auth.validation", { plugin: "hmac" })
45
- def self.stats
46
- Hooks::Core::GlobalComponents.stats
47
- end
48
-
49
- # Global failbot component accessor
50
- # @return [Hooks::Core::Failbot] Failbot instance for error reporting
51
- #
52
- # Provides access to the global failbot component for reporting errors
53
- # to services like Sentry, Rollbar, etc.
54
- #
55
- # @example Reporting an error in an inherited class
56
- # failbot.report("Auth validation failed", { plugin: "hmac" })
57
- def self.failbot
58
- Hooks::Core::GlobalComponents.failbot
59
- end
60
-
61
28
  # Retrieve the secret from the environment variable based on the key set in the configuration
62
29
  #
63
30
  # Note: This method is intended to be used by subclasses
@@ -3,6 +3,7 @@
3
3
  require "openssl"
4
4
  require "time"
5
5
  require_relative "base"
6
+ require_relative "timestamp_validator"
6
7
 
7
8
  module Hooks
8
9
  module Plugins
@@ -39,6 +40,7 @@ module Hooks
39
40
  DEFAULT_CONFIG = {
40
41
  algorithm: "sha256",
41
42
  format: "algorithm=signature", # Format: algorithm=hash
43
+ header: "X-Signature", # Default header containing the signature
42
44
  timestamp_tolerance: 300, # 5 minutes tolerance for timestamp validation
43
45
  version_prefix: "v0" # Default version prefix for versioned signatures
44
46
  }.freeze
@@ -117,7 +119,10 @@ module Hooks
117
119
 
118
120
  # Validate timestamp if required (for services that include timestamp validation)
119
121
  if validator_config[:timestamp_header]
120
- return false unless valid_timestamp?(normalized_headers, validator_config)
122
+ unless valid_timestamp?(normalized_headers, validator_config)
123
+ log.warn("Auth::HMAC validation failed: Invalid timestamp")
124
+ return false
125
+ end
121
126
  end
122
127
 
123
128
  # Compute expected signature
@@ -153,7 +158,7 @@ module Hooks
153
158
  tolerance = validator_config[:timestamp_tolerance] || DEFAULT_CONFIG[:timestamp_tolerance]
154
159
 
155
160
  DEFAULT_CONFIG.merge({
156
- header: validator_config[:header] || "X-Signature",
161
+ header: validator_config[:header] || DEFAULT_CONFIG[:header],
157
162
  timestamp_header: validator_config[:timestamp_header],
158
163
  timestamp_tolerance: tolerance,
159
164
  algorithm: algorithm,
@@ -180,6 +185,7 @@ module Hooks
180
185
  #
181
186
  # Checks if the provided timestamp is within the configured tolerance
182
187
  # of the current time. This prevents replay attacks using old requests.
188
+ # Supports both ISO 8601 UTC timestamps and Unix timestamps.
183
189
  #
184
190
  # @param headers [Hash<String, String>] Normalized HTTP headers
185
191
  # @param config [Hash<Symbol, Object>] Validator configuration
@@ -189,25 +195,21 @@ module Hooks
189
195
  # @api private
190
196
  def self.valid_timestamp?(headers, config)
191
197
  timestamp_header = config[:timestamp_header]
198
+ tolerance = config[:timestamp_tolerance] || 300
192
199
  return false unless timestamp_header
193
200
 
194
- timestamp_header = timestamp_header.downcase
195
- timestamp_value = headers[timestamp_header]
196
-
201
+ timestamp_value = headers[timestamp_header.downcase]
197
202
  return false unless timestamp_value
198
203
 
199
- # Security: Strict timestamp validation - must be only digits with no leading zeros
200
- return false unless timestamp_value.match?(/\A[1-9]\d*\z/) || timestamp_value == "0"
201
-
202
- timestamp = timestamp_value.to_i
203
-
204
- # Ensure timestamp is a positive integer (reject zero and negative)
205
- return false unless timestamp > 0
206
-
207
- current_time = Time.now.to_i
208
- tolerance = config[:timestamp_tolerance]
204
+ timestamp_validator.valid?(timestamp_value, tolerance)
205
+ end
209
206
 
210
- (current_time - timestamp).abs <= tolerance
207
+ # Get timestamp validator instance
208
+ #
209
+ # @return [TimestampValidator] Singleton timestamp validator instance
210
+ # @api private
211
+ def self.timestamp_validator
212
+ @timestamp_validator ||= TimestampValidator.new
211
213
  end
212
214
 
213
215
  # Compute HMAC signature based on configuration requirements
@@ -257,7 +259,7 @@ module Hooks
257
259
  # - {body}: Replaced with the raw payload
258
260
  # @example Template usage
259
261
  # template: "{version}:{timestamp}:{body}"
260
- # result: "v0:1609459200:{"event":"push"}"
262
+ # result: "v0:1609459200:{\"event\":\"push\"}"
261
263
  # @api private
262
264
  def self.build_signing_payload(payload:, headers:, config:)
263
265
  template = config[:payload_template]
@@ -287,7 +289,7 @@ module Hooks
287
289
  # - :algorithm_prefixed: "sha256=abc123..." (GitHub style)
288
290
  # - :hash_only: "abc123..." (Shopify style)
289
291
  # - :version_prefixed: "v0=abc123..." (Slack style)
290
- # @note Defaults to algorithm_prefixed format for unknown format styles
292
+ # @note Defaults to algorithm-prefixed format for unknown format styles
291
293
  # @api private
292
294
  def self.format_signature(hash, config)
293
295
  format_style = FORMATS[config[:format]]
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Auth
8
+ # Validates and parses timestamps for webhook authentication
9
+ #
10
+ # This class provides secure timestamp validation supporting both
11
+ # ISO 8601 UTC format and Unix timestamp format. It includes
12
+ # strict validation to prevent various injection attacks.
13
+ #
14
+ # @example Basic usage
15
+ # validator = TimestampValidator.new
16
+ # validator.valid?("1609459200", 300) # => true/false
17
+ # validator.parse("2021-01-01T00:00:00Z") # => 1609459200
18
+ #
19
+ # @api private
20
+ class TimestampValidator
21
+ # Validate timestamp against current time with tolerance
22
+ #
23
+ # @param timestamp_value [String] The timestamp string to validate
24
+ # @param tolerance [Integer] Maximum age in seconds (default: 300)
25
+ # @return [Boolean] true if timestamp is valid and within tolerance
26
+ def valid?(timestamp_value, tolerance = 300)
27
+ return false if timestamp_value.nil? || timestamp_value.strip.empty?
28
+
29
+ parsed_timestamp = parse(timestamp_value.strip)
30
+ return false unless parsed_timestamp.is_a?(Integer)
31
+
32
+ now = Time.now.utc.to_i
33
+ (now - parsed_timestamp).abs <= tolerance
34
+ end
35
+
36
+ # Parse timestamp value supporting both ISO 8601 UTC and Unix formats
37
+ #
38
+ # @param timestamp_value [String] The timestamp string to parse
39
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
40
+ # @note Security: Strict validation prevents various injection attacks
41
+ def parse(timestamp_value)
42
+ return nil if invalid_characters?(timestamp_value)
43
+
44
+ parse_iso8601_timestamp(timestamp_value) || parse_unix_timestamp(timestamp_value)
45
+ end
46
+
47
+ private
48
+
49
+ # Check for control characters, whitespace, or null bytes
50
+ #
51
+ # @param timestamp_value [String] The timestamp to check
52
+ # @return [Boolean] true if contains invalid characters
53
+ def invalid_characters?(timestamp_value)
54
+ if timestamp_value =~ /[\u0000-\u001F\u007F-\u009F]/
55
+ log_warning("Timestamp contains invalid characters")
56
+ true
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ # Parse ISO 8601 UTC timestamp string (must have UTC indicator)
63
+ #
64
+ # @param timestamp_value [String] ISO 8601 timestamp string
65
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
66
+ def parse_iso8601_timestamp(timestamp_value)
67
+ # Handle space-separated format and convert to standard ISO format
68
+ if timestamp_value =~ /\A(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)(?: )\+0000\z/
69
+ timestamp_value = "#{$1}T#{$2}+00:00"
70
+ end
71
+
72
+ # Ensure the timestamp explicitly includes a UTC indicator
73
+ return nil unless timestamp_value =~ /(Z|\+00:00|\+0000)\z/
74
+ return nil unless iso8601_format?(timestamp_value)
75
+
76
+ parsed_time = parse_time_safely(timestamp_value)
77
+ return nil unless parsed_time&.utc_offset&.zero?
78
+
79
+ parsed_time.to_i
80
+ end
81
+
82
+ # Parse Unix timestamp string (must be positive integer, no leading zeros except for "0")
83
+ #
84
+ # @param timestamp_value [String] Unix timestamp string
85
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
86
+ def parse_unix_timestamp(timestamp_value)
87
+ return nil unless unix_format?(timestamp_value)
88
+
89
+ ts = timestamp_value.to_i
90
+ return nil if ts <= 0
91
+
92
+ ts
93
+ end
94
+
95
+ # Check if timestamp string looks like ISO 8601 format
96
+ #
97
+ # @param timestamp_value [String] The timestamp string to check
98
+ # @return [Boolean] true if it appears to be ISO 8601 format
99
+ def iso8601_format?(timestamp_value)
100
+ !!(timestamp_value =~ /\A\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(Z|\+00:00|\+0000)?\z/)
101
+ end
102
+
103
+ # Check if timestamp string looks like Unix timestamp format
104
+ #
105
+ # @param timestamp_value [String] The timestamp string to check
106
+ # @return [Boolean] true if it appears to be Unix timestamp format
107
+ def unix_format?(timestamp_value)
108
+ return true if timestamp_value == "0"
109
+ !!(timestamp_value =~ /\A[1-9]\d*\z/)
110
+ end
111
+
112
+ # Safely parse time string with error handling
113
+ #
114
+ # @param timestamp_value [String] The timestamp string to parse
115
+ # @return [Time, nil] Parsed time object or nil if parsing fails
116
+ def parse_time_safely(timestamp_value)
117
+ Time.parse(timestamp_value)
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+
122
+ # Log warning message
123
+ #
124
+ # @param message [String] Warning message to log
125
+ def log_warning(message)
126
+ return unless defined?(Hooks::Log) && Hooks::Log.instance
127
+
128
+ Hooks::Log.instance.warn("Auth::TimestampValidator validation failed: #{message}")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../core/global_components"
4
+ require_relative "../../core/component_access"
4
5
 
5
6
  module Hooks
6
7
  module Plugins
@@ -9,6 +10,8 @@ module Hooks
9
10
  #
10
11
  # All custom handlers must inherit from this class and implement the #call method
11
12
  class Base
13
+ include Hooks::Core::ComponentAccess
14
+
12
15
  # Process a webhook request
13
16
  #
14
17
  # @param payload [Hash, String] Parsed request body (JSON Hash) or raw string
@@ -19,42 +22,6 @@ module Hooks
19
22
  def call(payload:, headers:, config:)
20
23
  raise NotImplementedError, "Handler must implement #call method"
21
24
  end
22
-
23
- # Short logger accessor for all subclasses
24
- # @return [Hooks::Log] Logger instance
25
- #
26
- # Provides a convenient way for handlers to log messages without needing
27
- # to reference the full Hooks::Log namespace.
28
- #
29
- # @example Logging an error in an inherited class
30
- # log.error("oh no an error occured")
31
- def log
32
- Hooks::Log.instance
33
- end
34
-
35
- # Global stats component accessor
36
- # @return [Hooks::Core::Stats] Stats instance for metrics reporting
37
- #
38
- # Provides access to the global stats component for reporting metrics
39
- # to services like DataDog, New Relic, etc.
40
- #
41
- # @example Recording a metric in an inherited class
42
- # stats.increment("webhook.processed", { handler: "MyHandler" })
43
- def stats
44
- Hooks::Core::GlobalComponents.stats
45
- end
46
-
47
- # Global failbot component accessor
48
- # @return [Hooks::Core::Failbot] Failbot instance for error reporting
49
- #
50
- # Provides access to the global failbot component for reporting errors
51
- # to services like Sentry, Rollbar, etc.
52
- #
53
- # @example Reporting an error in an inherited class
54
- # failbot.report("Something went wrong", { handler: "MyHandler" })
55
- def failbot
56
- Hooks::Core::GlobalComponents.failbot
57
- end
58
25
  end
59
26
  end
60
27
  end
@@ -1,8 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Default handler when no custom handler is found
4
- # This handler simply acknowledges receipt of the webhook and shows a few of the built-in features
3
+ # Default webhook handler implementation
4
+ #
5
+ # This handler provides a basic webhook processing implementation that can be used
6
+ # as a fallback when no custom handler is configured for an endpoint. It demonstrates
7
+ # the standard handler interface and provides basic logging functionality.
8
+ #
9
+ # @example Usage in endpoint configuration
10
+ # handler:
11
+ # type: DefaultHandler
12
+ #
13
+ # @see Hooks::Plugins::Handlers::Base
5
14
  class DefaultHandler < Hooks::Plugins::Handlers::Base
15
+ # Process a webhook request with basic acknowledgment
16
+ #
17
+ # Provides a simple webhook processing implementation that logs the request
18
+ # and returns a standard acknowledgment response. This is useful for testing
19
+ # webhook endpoints or as a placeholder during development.
20
+ #
21
+ # @param payload [Hash, String] The webhook payload (parsed JSON or raw string)
22
+ # @param headers [Hash<String, String>] HTTP headers from the webhook request
23
+ # @param config [Hash] Endpoint configuration containing handler options
24
+ # @return [Hash] Response indicating successful processing
25
+ # @option config [Hash] :opts Additional handler-specific configuration options
26
+ #
27
+ # @example Basic usage
28
+ # handler = DefaultHandler.new
29
+ # response = handler.call(
30
+ # payload: { "event" => "push" },
31
+ # headers: { "Content-Type" => "application/json" },
32
+ # config: { opts: {} }
33
+ # )
34
+ # # => { message: "webhook processed successfully", handler: "DefaultHandler", timestamp: "..." }
6
35
  def call(payload:, headers:, config:)
7
36
 
8
37
  log.info("🔔 Default handler invoked for webhook 🔔")
@@ -15,7 +44,7 @@ class DefaultHandler < Hooks::Plugins::Handlers::Base
15
44
  {
16
45
  message: "webhook processed successfully",
17
46
  handler: "DefaultHandler",
18
- timestamp: Time.now.iso8601
47
+ timestamp: Time.now.utc.iso8601
19
48
  }
20
49
  end
21
50
  end
@@ -7,11 +7,25 @@ module Hooks
7
7
  module Instruments
8
8
  # Default failbot instrument implementation
9
9
  #
10
- # This is a stub implementation that does nothing by default.
11
- # Users can replace this with their own implementation for services
12
- # like Sentry, Rollbar, etc.
10
+ # This is a no-op implementation that provides the error reporting interface
11
+ # without actually sending errors anywhere. It serves as a safe default when
12
+ # no custom error reporting implementation is configured.
13
+ #
14
+ # Users should replace this with their own implementation for services
15
+ # like Sentry, Rollbar, Honeybadger, etc.
16
+ #
17
+ # @example Replacing with a custom implementation
18
+ # # In your application initialization:
19
+ # custom_failbot = MySentryFailbotImplementation.new
20
+ # Hooks::Core::GlobalComponents.failbot = custom_failbot
21
+ #
22
+ # @see Hooks::Plugins::Instruments::FailbotBase
23
+ # @see Hooks::Core::GlobalComponents
13
24
  class Failbot < FailbotBase
14
- # Inherit from FailbotBase to provide a default implementation of the failbot instrument.
25
+ # Inherit from FailbotBase to provide a default no-op implementation
26
+ # of the error reporting instrument interface.
27
+ #
28
+ # All methods from FailbotBase are inherited and provide safe no-op behavior.
15
29
  end
16
30
  end
17
31
  end
@@ -1,23 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../core/component_access"
4
+
3
5
  module Hooks
4
6
  module Plugins
5
7
  module Instruments
6
8
  # Base class for all failbot instrument plugins
7
9
  #
8
- # All custom failbot implementations must inherit from this class and implement
9
- # the required methods for error reporting.
10
+ # This class provides the foundation for implementing custom error reporting
11
+ # instruments. Subclasses should implement specific methods for their target
12
+ # error reporting service (Sentry, Rollbar, Honeybadger, etc.).
13
+ #
14
+ # @abstract Subclass and implement service-specific error reporting methods
15
+ # @example Implementing a custom failbot instrument
16
+ # class MySentryFailbot < Hooks::Plugins::Instruments::FailbotBase
17
+ # def report(error_or_message, context = {})
18
+ # case error_or_message
19
+ # when Exception
20
+ # Sentry.capture_exception(error_or_message, extra: context)
21
+ # else
22
+ # Sentry.capture_message(error_or_message.to_s, extra: context)
23
+ # end
24
+ # log.debug("Reported error to Sentry")
25
+ # end
26
+ # end
27
+ #
28
+ # @see Hooks::Plugins::Instruments::Failbot
10
29
  class FailbotBase
11
- # Short logger accessor for all subclasses
12
- # @return [Hooks::Log] Logger instance
30
+ include Hooks::Core::ComponentAccess
31
+
32
+ # Report an error or message to the error tracking service
33
+ #
34
+ # This is a no-op implementation that subclasses should override
35
+ # to provide actual error reporting functionality.
36
+ #
37
+ # @param error_or_message [Exception, String] The error to report or message string
38
+ # @param context [Hash] Additional context information about the error
39
+ # @return [void]
40
+ # @note Subclasses should implement this method for their specific service
41
+ # @example Override in subclass
42
+ # def report(error_or_message, context = {})
43
+ # if error_or_message.is_a?(Exception)
44
+ # ErrorService.report_exception(error_or_message, context)
45
+ # else
46
+ # ErrorService.report_message(error_or_message, context)
47
+ # end
48
+ # end
49
+ def report(error_or_message, context = {})
50
+ # No-op implementation for base class
51
+ end
52
+
53
+ # Report a warning-level message
13
54
  #
14
- # Provides a convenient way for instruments to log messages without needing
15
- # to reference the full Hooks::Log namespace.
55
+ # This is a no-op implementation that subclasses should override
56
+ # to provide actual warning reporting functionality.
16
57
  #
17
- # @example Logging debug info in an inherited class
18
- # log.debug("Sending error to external service")
19
- def log
20
- Hooks::Log.instance
58
+ # @param message [String] Warning message to report
59
+ # @param context [Hash] Additional context information
60
+ # @return [void]
61
+ # @note Subclasses should implement this method for their specific service
62
+ # @example Override in subclass
63
+ # def warn(message, context = {})
64
+ # ErrorService.report_warning(message, context)
65
+ # end
66
+ def warn(message, context = {})
67
+ # No-op implementation for base class
21
68
  end
22
69
  end
23
70
  end
@@ -7,11 +7,25 @@ module Hooks
7
7
  module Instruments
8
8
  # Default stats instrument implementation
9
9
  #
10
- # This is a stub implementation that does nothing by default.
11
- # Users can replace this with their own implementation for services
12
- # like DataDog, New Relic, etc.
10
+ # This is a no-op implementation that provides the stats interface without
11
+ # actually sending metrics anywhere. It serves as a safe default when no
12
+ # custom stats implementation is configured.
13
+ #
14
+ # Users should replace this with their own implementation for services
15
+ # like DataDog, New Relic, StatsD, etc.
16
+ #
17
+ # @example Replacing with a custom implementation
18
+ # # In your application initialization:
19
+ # custom_stats = MyCustomStatsImplementation.new
20
+ # Hooks::Core::GlobalComponents.stats = custom_stats
21
+ #
22
+ # @see Hooks::Plugins::Instruments::StatsBase
23
+ # @see Hooks::Core::GlobalComponents
13
24
  class Stats < StatsBase
14
- # Inherit from StatsBase to provide a default implementation of the stats instrument.
25
+ # Inherit from StatsBase to provide a default no-op implementation
26
+ # of the stats instrument interface.
27
+ #
28
+ # All methods from StatsBase are inherited and provide safe no-op behavior.
15
29
  end
16
30
  end
17
31
  end
@@ -1,23 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../core/component_access"
4
+
3
5
  module Hooks
4
6
  module Plugins
5
7
  module Instruments
6
8
  # Base class for all stats instrument plugins
7
9
  #
8
- # All custom stats implementations must inherit from this class and implement
9
- # the required methods for metrics reporting.
10
+ # This class provides the foundation for implementing custom metrics reporting
11
+ # instruments. Subclasses should implement specific methods for their target
12
+ # metrics service (DataDog, New Relic, StatsD, etc.).
13
+ #
14
+ # @abstract Subclass and implement service-specific metrics methods
15
+ # @example Implementing a custom stats instrument
16
+ # class MyStatsImplementation < Hooks::Plugins::Instruments::StatsBase
17
+ # def increment(metric_name, tags = {})
18
+ # # Send increment metric to your service
19
+ # MyMetricsService.increment(metric_name, tags)
20
+ # log.debug("Sent increment metric: #{metric_name}")
21
+ # end
22
+ #
23
+ # def timing(metric_name, duration, tags = {})
24
+ # # Send timing metric to your service
25
+ # MyMetricsService.timing(metric_name, duration, tags)
26
+ # end
27
+ # end
28
+ #
29
+ # @see Hooks::Plugins::Instruments::Stats
10
30
  class StatsBase
11
- # Short logger accessor for all subclasses
12
- # @return [Hooks::Log] Logger instance
31
+ include Hooks::Core::ComponentAccess
32
+
33
+ # Record an increment metric
34
+ #
35
+ # This is a no-op implementation that subclasses should override
36
+ # to provide actual metrics reporting functionality.
37
+ #
38
+ # @param metric_name [String] Name of the metric to increment
39
+ # @param tags [Hash] Optional tags/labels for the metric
40
+ # @return [void]
41
+ # @note Subclasses should implement this method for their specific service
42
+ # @example Override in subclass
43
+ # def increment(metric_name, tags = {})
44
+ # statsd.increment(metric_name, tags: tags)
45
+ # end
46
+ def increment(metric_name, tags = {})
47
+ # No-op implementation for base class
48
+ end
49
+
50
+ # Record a timing/duration metric
51
+ #
52
+ # This is a no-op implementation that subclasses should override
53
+ # to provide actual metrics reporting functionality.
54
+ #
55
+ # @param metric_name [String] Name of the timing metric
56
+ # @param duration [Numeric] Duration value (typically in milliseconds)
57
+ # @param tags [Hash] Optional tags/labels for the metric
58
+ # @return [void]
59
+ # @note Subclasses should implement this method for their specific service
60
+ # @example Override in subclass
61
+ # def timing(metric_name, duration, tags = {})
62
+ # statsd.timing(metric_name, duration, tags: tags)
63
+ # end
64
+ def timing(metric_name, duration, tags = {})
65
+ # No-op implementation for base class
66
+ end
67
+
68
+ # Record a gauge metric
13
69
  #
14
- # Provides a convenient way for instruments to log messages without needing
15
- # to reference the full Hooks::Log namespace.
70
+ # This is a no-op implementation that subclasses should override
71
+ # to provide actual metrics reporting functionality.
16
72
  #
17
- # @example Logging an error in an inherited class
18
- # log.error("Failed to send metric to external service")
19
- def log
20
- Hooks::Log.instance
73
+ # @param metric_name [String] Name of the gauge metric
74
+ # @param value [Numeric] Current value for the gauge
75
+ # @param tags [Hash] Optional tags/labels for the metric
76
+ # @return [void]
77
+ # @note Subclasses should implement this method for their specific service
78
+ # @example Override in subclass
79
+ # def gauge(metric_name, value, tags = {})
80
+ # statsd.gauge(metric_name, value, tags: tags)
81
+ # end
82
+ def gauge(metric_name, value, tags = {})
83
+ # No-op implementation for base class
21
84
  end
22
85
  end
23
86
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../core/global_components"
4
+ require_relative "../core/component_access"
4
5
 
5
6
  module Hooks
6
7
  module Plugins
@@ -8,6 +9,8 @@ module Hooks
8
9
  #
9
10
  # Plugins can hook into request/response/error lifecycle events
10
11
  class Lifecycle
12
+ include Hooks::Core::ComponentAccess
13
+
11
14
  # Called before handler execution
12
15
  #
13
16
  # @param env [Hash] Rack environment
@@ -30,42 +33,6 @@ module Hooks
30
33
  def on_error(exception, env)
31
34
  # Override in subclass for error handling logic
32
35
  end
33
-
34
- # Short logger accessor for all subclasses
35
- # @return [Hooks::Log] Logger instance
36
- #
37
- # Provides a convenient way for lifecycle plugins to log messages without needing
38
- # to reference the full Hooks::Log namespace.
39
- #
40
- # @example Logging an error in an inherited class
41
- # log.error("oh no an error occured")
42
- def log
43
- Hooks::Log.instance
44
- end
45
-
46
- # Global stats component accessor
47
- # @return [Hooks::Core::Stats] Stats instance for metrics reporting
48
- #
49
- # Provides access to the global stats component for reporting metrics
50
- # to services like DataDog, New Relic, etc.
51
- #
52
- # @example Recording a metric in an inherited class
53
- # stats.increment("lifecycle.request_processed")
54
- def stats
55
- Hooks::Core::GlobalComponents.stats
56
- end
57
-
58
- # Global failbot component accessor
59
- # @return [Hooks::Core::Failbot] Failbot instance for error reporting
60
- #
61
- # Provides access to the global failbot component for reporting errors
62
- # to services like Sentry, Rollbar, etc.
63
- #
64
- # @example Reporting an error in an inherited class
65
- # failbot.report("Lifecycle hook failed")
66
- def failbot
67
- Hooks::Core::GlobalComponents.failbot
68
- end
69
36
  end
70
37
  end
71
38
  end
data/lib/hooks/version.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Main Hooks module containing version information
3
4
  module Hooks
4
- VERSION = "0.0.3"
5
+ # Current version of the Hooks webhook framework
6
+ # @return [String] The version string following semantic versioning
7
+ VERSION = "0.0.4".freeze
5
8
  end
data/lib/hooks.rb CHANGED
@@ -2,21 +2,29 @@
2
2
 
3
3
  require_relative "hooks/version"
4
4
  require_relative "hooks/core/builder"
5
-
6
- # Load all core components
7
- Dir[File.join(__dir__, "hooks/core/**/*.rb")].sort.each do |file|
8
- require file
9
- end
10
-
11
- # Load all plugins (auth plugins, handler plugins, lifecycle hooks, etc.)
12
- Dir[File.join(__dir__, "hooks/plugins/**/*.rb")].sort.each do |file|
13
- require file
14
- end
15
-
16
- # Load all utils
17
- Dir[File.join(__dir__, "hooks/utils/**/*.rb")].sort.each do |file|
18
- require file
19
- end
5
+ require_relative "hooks/core/config_loader"
6
+ require_relative "hooks/core/config_validator"
7
+ require_relative "hooks/core/logger_factory"
8
+ require_relative "hooks/core/plugin_loader"
9
+ require_relative "hooks/core/global_components"
10
+ require_relative "hooks/core/component_access"
11
+ require_relative "hooks/core/log"
12
+ require_relative "hooks/core/failbot"
13
+ require_relative "hooks/core/stats"
14
+ require_relative "hooks/plugins/auth/base"
15
+ require_relative "hooks/plugins/auth/hmac"
16
+ require_relative "hooks/plugins/auth/shared_secret"
17
+ require_relative "hooks/plugins/handlers/base"
18
+ require_relative "hooks/plugins/handlers/default"
19
+ require_relative "hooks/plugins/lifecycle"
20
+ require_relative "hooks/plugins/instruments/stats_base"
21
+ require_relative "hooks/plugins/instruments/failbot_base"
22
+ require_relative "hooks/plugins/instruments/stats"
23
+ require_relative "hooks/plugins/instruments/failbot"
24
+ require_relative "hooks/utils/normalize"
25
+ require_relative "hooks/utils/retry"
26
+ require_relative "hooks/security"
27
+ require_relative "hooks/version"
20
28
 
21
29
  # Main module for the Hooks webhook server framework
22
30
  module Hooks
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.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - github
@@ -130,6 +130,7 @@ files:
130
130
  - lib/hooks/app/endpoints/version.rb
131
131
  - lib/hooks/app/helpers.rb
132
132
  - lib/hooks/core/builder.rb
133
+ - lib/hooks/core/component_access.rb
133
134
  - lib/hooks/core/config_loader.rb
134
135
  - lib/hooks/core/config_validator.rb
135
136
  - lib/hooks/core/failbot.rb
@@ -141,6 +142,7 @@ files:
141
142
  - lib/hooks/plugins/auth/base.rb
142
143
  - lib/hooks/plugins/auth/hmac.rb
143
144
  - lib/hooks/plugins/auth/shared_secret.rb
145
+ - lib/hooks/plugins/auth/timestamp_validator.rb
144
146
  - lib/hooks/plugins/handlers/base.rb
145
147
  - lib/hooks/plugins/handlers/default.rb
146
148
  - lib/hooks/plugins/instruments/failbot.rb