hooks-ruby 0.0.2 → 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: 7588a4a6fdf58e7c927dc4b20b9fa330db86db672f5c779a4c46d51e7e41f386
4
- data.tar.gz: 02bdb939c52fd501387856f432a11a82f842d25053d9e73ef3967eb12be03c7a
3
+ metadata.gz: 2cab1bf1a1f61011d053be5beb9d48738bd439303b87743e41e10df0b4b9d65c
4
+ data.tar.gz: 9b9abc62fff8f0f2f77ad94949bc7e067fff5a2f072bbf73f8e6e55c03865714
5
5
  SHA512:
6
- metadata.gz: f11eddcaf09095e1d68bcfeeeba3660eab7e3a9bf42744d780987bfb4b54db7cfa6691c4cc435c5dbda4eb3ee60065a6f3c6239df87bf5b5587f638bb3c8d094
7
- data.tar.gz: af806e9c8144ed2da97e723c3a4dfcb54f2fd8db85970e806aa3f2f56e6ef8ca665ae157066f1456126804c2bba8ba573d5f59f717de0249148d81d11f9e848d
6
+ metadata.gz: 40f769a697bd61c6c851eef217fadc66387c49d37c19639a283dd770ca8be8889a13eeba2785a41c53edcdef29327231527405a9de21b2b4620416196da792c2
7
+ data.tar.gz: 119a371d927f240aefe4e74c822b48318032250e7cbf060a9bd077b1375823137b419f7d1e2d27d775e6468d38843afb45ec9c8cd8a71a22d82226160c66043e
data/README.md CHANGED
@@ -66,13 +66,13 @@ 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
73
73
  ```
74
74
 
75
- That is pretty much it! Below you will find more detailed instructions on how to install and use Hooks, as well as how to create your own plugins. This high-level overview should give you a good idea of how Hooks works and how you can use it to handle webhooks in your applications. You may also be interested in using your own custom authentication plugins to secure your webhook endpoints, which is covered in the [Authentication](#authentication) section below.
75
+ That is pretty much it! Below you will find more detailed instructions on how to install and use Hooks, as well as how to create your own plugins. This high-level overview should give you a good idea of how Hooks works and how you can use it to handle webhooks in your applications. You may also be interested in using your own custom authentication plugins to secure your webhook endpoints, which is covered in the [Authentication](#authentication-plugins) section below.
76
76
 
77
77
  ## Installation 💎
78
78
 
@@ -102,7 +102,7 @@ First, create a `config.ru` file:
102
102
 
103
103
  ```ruby
104
104
  # file: config.ru
105
- require "hooks-ruby"
105
+ require "hooks"
106
106
 
107
107
  # See the config documentation below for the full list of available options
108
108
  # For this example, we will just set use_catchall_route to true
@@ -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
@@ -286,10 +286,26 @@ What these steps have done is set up a Hooks server that listens for incoming we
286
286
 
287
287
  To see a live working version of this example, check out the [`spec/acceptance/`](spec/acceptance/) directory in this repository, which is used for acceptance testing the Hooks framework. It contains a complete example of how to set up a Hooks server with custom plugins, authentication, and more.
288
288
 
289
- ### Authentication
289
+ ### Authentication Plugins
290
290
 
291
291
  See the [Auth Plugins](docs/auth_plugins.md) documentation for even more information on how to create your own custom authentication plugins.
292
292
 
293
+ ### Handler Plugins
294
+
295
+ See the [Handler Plugins](docs/handler_plugins.md) documentation for in-depth information about handler plugins and how you can create your own to extend the functionality of the Hooks framework for your own deployment.
296
+
297
+ ### Lifecycle Plugins
298
+
299
+ See the [Lifecycle Plugins](docs/lifecycle_plugins.md) documentation for information on how to create lifecycle plugins that can hook into the request/response/error lifecycle of the Hooks framework, allowing you to add custom behavior at various stages of processing webhook requests.
300
+
301
+ ### Instrument Plugins
302
+
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
+
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
+
293
309
  ## Contributing 🤝
294
310
 
295
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/hooks.gemspec CHANGED
@@ -22,7 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5"
23
23
  spec.add_dependency "dry-schema", "~> 1.14", ">= 1.14.1"
24
24
  spec.add_dependency "grape", "~> 2.3"
25
- spec.add_dependency "grape-swagger", "~> 2.1", ">= 2.1.2"
26
25
  spec.add_dependency "puma", "~> 6.6"
27
26
 
28
27
  spec.required_ruby_version = Gem::Requirement.new(">= 3.2.2")
data/lib/hooks/app/api.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "../plugins/handlers/base"
9
9
  require_relative "../plugins/handlers/default"
10
10
  require_relative "../core/logger_factory"
11
11
  require_relative "../core/log"
12
+ require_relative "../core/plugin_loader"
12
13
 
13
14
  # Import all core endpoint classes dynamically
14
15
  Dir[File.join(__dir__, "endpoints/**/*.rb")].sort.each { |file| require file }
@@ -21,14 +22,13 @@ module Hooks
21
22
  include Hooks::App::Auth
22
23
 
23
24
  class << self
24
- attr_reader :start_time
25
+ attr_reader :server_start_time
25
26
  end
26
27
 
27
28
  # Create a new configured API class
28
29
  def self.create(config:, endpoints:, log:)
29
- @start_time = Time.now
30
-
31
- Hooks::Log.instance = log
30
+ # :nocov:
31
+ @server_start_time = Time.now
32
32
 
33
33
  api_class = Class.new(Grape::API) do
34
34
  content_type :json, "application/json"
@@ -48,23 +48,57 @@ module Hooks
48
48
  endpoints.each do |endpoint_config|
49
49
  full_path = "#{config[:root_path]}#{endpoint_config[:path]}"
50
50
  handler_class_name = endpoint_config[:handler]
51
+ http_method = (endpoint_config[:method] || "post").downcase.to_sym
51
52
 
52
- post(full_path) do
53
+ send(http_method, full_path) do
53
54
  request_id = uuid
55
+ start_time = Time.now
56
+
54
57
  request_context = {
55
58
  request_id:,
56
59
  path: full_path,
57
60
  handler: handler_class_name
58
61
  }
59
62
 
63
+ # everything wrapped in the log context has access to the request context and includes it in log messages
64
+ # ex: Hooks::Log.info("message") will include request_id, path, handler, etc
60
65
  Core::LogContext.with(request_context) do
61
66
  begin
67
+ # Build Rack environment for lifecycle hooks
68
+ rack_env = {
69
+ "REQUEST_METHOD" => request.request_method,
70
+ "PATH_INFO" => request.path_info,
71
+ "QUERY_STRING" => request.query_string,
72
+ "HTTP_VERSION" => request.env["HTTP_VERSION"],
73
+ "REQUEST_URI" => request.url,
74
+ "SERVER_NAME" => request.env["SERVER_NAME"],
75
+ "SERVER_PORT" => request.env["SERVER_PORT"],
76
+ "CONTENT_TYPE" => request.content_type,
77
+ "CONTENT_LENGTH" => request.content_length,
78
+ "REMOTE_ADDR" => request.env["REMOTE_ADDR"],
79
+ "hooks.request_id" => request_id,
80
+ "hooks.handler" => handler_class_name,
81
+ "hooks.endpoint_config" => endpoint_config,
82
+ "hooks.start_time" => start_time.iso8601,
83
+ "hooks.full_path" => full_path
84
+ }
85
+
86
+ # Add HTTP headers to environment
87
+ headers.each do |key, value|
88
+ env_key = "HTTP_#{key.upcase.tr('-', '_')}"
89
+ rack_env[env_key] = value
90
+ end
91
+
92
+ # Call lifecycle hooks: on_request
93
+ Core::PluginLoader.lifecycle_plugins.each do |plugin|
94
+ plugin.on_request(rack_env)
95
+ end
96
+
62
97
  enforce_request_limits(config)
63
98
  request.body.rewind
64
99
  raw_body = request.body.read
65
100
 
66
101
  if endpoint_config[:auth]
67
- log.info "validating request (id: #{request_id}, handler: #{handler_class_name})"
68
102
  validate_auth!(raw_body, headers, endpoint_config, config)
69
103
  end
70
104
 
@@ -78,16 +112,29 @@ module Hooks
78
112
  config: endpoint_config
79
113
  )
80
114
 
81
- log.info "request processed successfully (id: #{request_id}, handler: #{handler_class_name})"
115
+ # Call lifecycle hooks: on_response
116
+ Core::PluginLoader.lifecycle_plugins.each do |plugin|
117
+ plugin.on_response(rack_env, response)
118
+ end
119
+
120
+ log.info("successfully processed webhook event with handler: #{handler_class_name}")
121
+ log.debug("processing duration: #{Time.now - start_time}s")
82
122
  status 200
83
123
  content_type "application/json"
84
- (response || { status: "ok" }).to_json
85
- rescue => e
86
- log.error "request failed: #{e.message} (id: #{request_id}, handler: #{handler_class_name})"
124
+ response.to_json
125
+ rescue StandardError => e
126
+ # Call lifecycle hooks: on_error
127
+ if defined?(rack_env)
128
+ Core::PluginLoader.lifecycle_plugins.each do |plugin|
129
+ plugin.on_error(e, rack_env)
130
+ end
131
+ end
132
+
133
+ log.error("an error occuring during the processing of a webhook event - #{e.message}")
87
134
  error_response = {
88
135
  error: e.message,
89
136
  code: determine_error_code(e),
90
- request_id: request_id
137
+ request_id:
91
138
  }
92
139
  error_response[:backtrace] = e.backtrace unless config[:production]
93
140
  status error_response[:code]
@@ -106,6 +153,7 @@ module Hooks
106
153
  end
107
154
 
108
155
  api_class
156
+ # :nocov:
109
157
  end
110
158
  end
111
159
  end
@@ -22,7 +22,7 @@ module Hooks
22
22
  def validate_auth!(payload, headers, endpoint_config, global_config = {})
23
23
  auth_config = endpoint_config[:auth]
24
24
 
25
- # Security: Ensure auth type is present and valid
25
+ # Ensure auth type is present and valid
26
26
  auth_type = auth_config&.dig(:type)
27
27
  unless auth_type&.is_a?(String) && !auth_type.strip.empty?
28
28
  error!("authentication configuration missing or invalid", 500)
@@ -32,17 +32,26 @@ module Hooks
32
32
  begin
33
33
  auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
34
34
  rescue => e
35
+ log.error("failed to load auth plugin '#{auth_type}': #{e.message}")
35
36
  error!("unsupported auth type '#{auth_type}'", 400)
36
37
  end
37
38
 
38
- unless auth_class.valid?(
39
- payload:,
40
- headers:,
41
- config: endpoint_config
42
- )
39
+ log.debug("validating auth for request with auth_class: #{auth_class.name}")
40
+ unless auth_class.valid?(payload:, headers:, config: endpoint_config)
41
+ log.warn("authentication failed for request with auth_class: #{auth_class.name}")
43
42
  error!("authentication failed", 401)
44
43
  end
45
44
  end
45
+
46
+ private
47
+
48
+ # Short logger accessor for auth module
49
+ # @return [Hooks::Log] Logger instance
50
+ #
51
+ # Provides access to the application logger for authentication operations.
52
+ def log
53
+ Hooks::Log.instance
54
+ end
46
55
  end
47
56
  end
48
57
  end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # !!! IMPORTANT !!!
4
+ # This file handles the catchall endpoint for the Hooks application.
5
+ # You should not be using catchall endpoints in production.
6
+ # This is mainly for development, testing, and demo purposes.
7
+ # The logging is limited, lifecycle hooks are not called,
8
+ # and it does not support plugins or instruments.
9
+ # Use with caution!
10
+
3
11
  require "grape"
4
12
  require_relative "../../plugins/handlers/default"
5
13
  require_relative "../helpers"
@@ -10,10 +18,13 @@ module Hooks
10
18
  include Hooks::App::Helpers
11
19
 
12
20
  def self.mount_path(config)
21
+ # :nocov:
13
22
  "#{config[:root_path]}/*path"
23
+ # :nocov:
14
24
  end
15
25
 
16
26
  def self.route_block(captured_config, captured_logger)
27
+ # :nocov:
17
28
  proc do
18
29
  request_id = uuid
19
30
 
@@ -23,7 +34,7 @@ module Hooks
23
34
 
24
35
  # Set request context for logging
25
36
  request_context = {
26
- request_id: request_id,
37
+ request_id:,
27
38
  path: "/#{params[:path]}",
28
39
  handler: "DefaultHandler"
29
40
  }
@@ -45,8 +56,8 @@ module Hooks
45
56
 
46
57
  # Call handler
47
58
  response = handler.call(
48
- payload: payload,
49
- headers: headers,
59
+ payload:,
60
+ headers:,
50
61
  config: {}
51
62
  )
52
63
 
@@ -78,6 +89,7 @@ module Hooks
78
89
  end
79
90
  end
80
91
  end
92
+ # :nocov:
81
93
  end
82
94
  end
83
95
  end
@@ -10,9 +10,9 @@ 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
- uptime_seconds: (Time.now - Hooks::App::API.start_time).to_i
15
+ uptime_seconds: (Time.now - Hooks::App::API.server_start_time).to_i
16
16
  }.to_json
17
17
  end
18
18
  end
@@ -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
@@ -5,6 +5,7 @@ require_relative "config_validator"
5
5
  require_relative "logger_factory"
6
6
  require_relative "plugin_loader"
7
7
  require_relative "../app/api"
8
+ require_relative "../utils/retry"
8
9
 
9
10
  module Hooks
10
11
  module Core
@@ -34,6 +35,11 @@ module Hooks
34
35
  )
35
36
  end
36
37
 
38
+ Hooks::Log.instance = @log
39
+
40
+ # Hydrate our Retryable instance
41
+ Retry.setup!(log: @log)
42
+
37
43
  # Load all plugins at boot time
38
44
  load_plugins(config)
39
45
 
@@ -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
@@ -15,6 +15,8 @@ module Hooks
15
15
  optional(:handler_dir).filled(:string) # For backward compatibility
16
16
  optional(:handler_plugin_dir).filled(:string)
17
17
  optional(:auth_plugin_dir).maybe(:string)
18
+ optional(:lifecycle_plugin_dir).maybe(:string)
19
+ optional(:instruments_plugin_dir).maybe(:string)
18
20
  optional(:log_level).filled(:string, included_in?: %w[debug info warn error])
19
21
  optional(:request_limit).filled(:integer, gt?: 0)
20
22
  optional(:request_timeout).filled(:integer, gt?: 0)
@@ -32,6 +34,7 @@ module Hooks
32
34
  ENDPOINT_CONFIG_SCHEMA = Dry::Schema.Params do
33
35
  required(:path).filled(:string)
34
36
  required(:handler).filled(:string)
37
+ optional(:method).filled(:string, included_in?: %w[get post put patch delete head options])
35
38
 
36
39
  optional(:auth).hash do
37
40
  required(:type).filled(:string)