otto 2.0.0.pre2 → 2.0.0.pre7

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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -3
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +116 -45
  10. data/Gemfile +5 -2
  11. data/Gemfile.lock +70 -24
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/ipaddr-encoding-quirk.md +34 -0
  17. data/docs/migrating/v2.0.0-pre2.md +11 -18
  18. data/examples/advanced_routes/README.md +137 -20
  19. data/examples/authentication_strategies/README.md +212 -19
  20. data/examples/authentication_strategies/config.ru +0 -1
  21. data/examples/backtrace_sanitization_demo.rb +86 -0
  22. data/examples/basic/README.md +61 -10
  23. data/examples/error_handler_registration.rb +136 -0
  24. data/examples/logging_improvements.rb +76 -0
  25. data/examples/mcp_demo/README.md +187 -27
  26. data/examples/security_features/README.md +249 -30
  27. data/examples/simple_geo_resolver.rb +107 -0
  28. data/lib/otto/core/configuration.rb +90 -45
  29. data/lib/otto/core/error_handler.rb +138 -8
  30. data/lib/otto/core/file_safety.rb +2 -2
  31. data/lib/otto/core/freezable.rb +93 -0
  32. data/lib/otto/core/middleware_stack.rb +25 -18
  33. data/lib/otto/core/router.rb +62 -9
  34. data/lib/otto/core/uri_generator.rb +2 -2
  35. data/lib/otto/core.rb +10 -0
  36. data/lib/otto/design_system.rb +2 -2
  37. data/lib/otto/env_keys.rb +65 -12
  38. data/lib/otto/helpers/base.rb +2 -2
  39. data/lib/otto/helpers/request.rb +85 -2
  40. data/lib/otto/helpers/response.rb +5 -5
  41. data/lib/otto/helpers/validation.rb +2 -2
  42. data/lib/otto/helpers.rb +6 -0
  43. data/lib/otto/locale/config.rb +56 -0
  44. data/lib/otto/locale/middleware.rb +160 -0
  45. data/lib/otto/locale.rb +10 -0
  46. data/lib/otto/logging_helpers.rb +273 -0
  47. data/lib/otto/mcp/auth/token.rb +2 -2
  48. data/lib/otto/mcp/protocol.rb +2 -2
  49. data/lib/otto/mcp/rate_limiting.rb +2 -2
  50. data/lib/otto/mcp/registry.rb +2 -2
  51. data/lib/otto/mcp/route_parser.rb +2 -2
  52. data/lib/otto/mcp/schema_validation.rb +2 -2
  53. data/lib/otto/mcp/server.rb +2 -2
  54. data/lib/otto/mcp.rb +5 -0
  55. data/lib/otto/privacy/config.rb +201 -0
  56. data/lib/otto/privacy/geo_resolver.rb +285 -0
  57. data/lib/otto/privacy/ip_privacy.rb +177 -0
  58. data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
  59. data/lib/otto/privacy.rb +31 -0
  60. data/lib/otto/response_handlers/auto.rb +2 -0
  61. data/lib/otto/response_handlers/base.rb +2 -0
  62. data/lib/otto/response_handlers/default.rb +2 -0
  63. data/lib/otto/response_handlers/factory.rb +2 -0
  64. data/lib/otto/response_handlers/json.rb +2 -0
  65. data/lib/otto/response_handlers/redirect.rb +2 -0
  66. data/lib/otto/response_handlers/view.rb +2 -0
  67. data/lib/otto/response_handlers.rb +2 -2
  68. data/lib/otto/route.rb +4 -4
  69. data/lib/otto/route_definition.rb +42 -15
  70. data/lib/otto/route_handlers/base.rb +2 -1
  71. data/lib/otto/route_handlers/class_method.rb +18 -25
  72. data/lib/otto/route_handlers/factory.rb +18 -16
  73. data/lib/otto/route_handlers/instance_method.rb +8 -5
  74. data/lib/otto/route_handlers/lambda.rb +8 -20
  75. data/lib/otto/route_handlers/logic_class.rb +25 -8
  76. data/lib/otto/route_handlers.rb +2 -2
  77. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
  78. data/lib/otto/security/authentication/auth_strategy.rb +13 -6
  79. data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
  80. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
  82. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  83. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  84. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  85. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  86. data/lib/otto/security/authentication.rb +5 -6
  87. data/lib/otto/security/authorization_error.rb +73 -0
  88. data/lib/otto/security/config.rb +53 -9
  89. data/lib/otto/security/configurator.rb +17 -15
  90. data/lib/otto/security/csrf.rb +2 -2
  91. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  92. data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
  93. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  94. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  95. data/lib/otto/security/rate_limiter.rb +2 -2
  96. data/lib/otto/security/rate_limiting.rb +2 -2
  97. data/lib/otto/security/validator.rb +2 -2
  98. data/lib/otto/security.rb +12 -0
  99. data/lib/otto/static.rb +2 -2
  100. data/lib/otto/utils.rb +27 -2
  101. data/lib/otto/version.rb +3 -3
  102. data/lib/otto.rb +344 -89
  103. data/otto.gemspec +9 -2
  104. metadata +72 -8
  105. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
@@ -1,58 +1,44 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/configuration.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative '../security/csrf'
6
6
  require_relative '../security/validator'
7
7
  require_relative '../security/authentication'
8
8
  require_relative '../security/rate_limiting'
9
9
  require_relative '../mcp/server'
10
+ require_relative 'freezable'
10
11
 
11
12
  class Otto
12
13
  module Core
13
14
  # Configuration module providing locale and application configuration methods
14
15
  module Configuration
15
- def configure_locale(opts)
16
- # Start with global configuration
17
- global_config = self.class.global_config
18
- @locale_config = nil
16
+ include Otto::Core::Freezable
19
17
 
20
- # Check if we have any locale configuration from any source
21
- has_global_locale = global_config && (global_config[:available_locales] || global_config[:default_locale])
18
+ def configure_locale(opts)
19
+ # Check if we have any locale configuration
22
20
  has_direct_options = opts[:available_locales] || opts[:default_locale]
23
21
  has_legacy_config = opts[:locale_config]
24
22
 
25
- # Only create locale_config if we have configuration from somewhere
26
- return unless has_global_locale || has_direct_options || has_legacy_config
23
+ # Only create locale_config if we have configuration
24
+ return unless has_direct_options || has_legacy_config
27
25
 
28
- @locale_config = {}
26
+ # Initialize with direct options
27
+ available_locales = opts[:available_locales]
28
+ default_locale = opts[:default_locale]
29
29
 
30
- # Apply global configuration first
31
- if global_config && global_config[:available_locales]
32
- @locale_config[:available_locales] =
33
- global_config[:available_locales]
34
- end
35
- if global_config && global_config[:default_locale]
36
- @locale_config[:default_locale] =
37
- global_config[:default_locale]
30
+ # Legacy support: Configure locale if provided via locale_config hash
31
+ if opts[:locale_config]
32
+ locale_opts = opts[:locale_config]
33
+ available_locales ||= locale_opts[:available_locales] || locale_opts[:available]
34
+ default_locale ||= locale_opts[:default_locale] || locale_opts[:default]
38
35
  end
39
36
 
40
- # Apply direct instance options (these override global config)
41
- @locale_config[:available_locales] = opts[:available_locales] if opts[:available_locales]
42
- @locale_config[:default_locale] = opts[:default_locale] if opts[:default_locale]
43
-
44
- # Legacy support: Configure locale if provided in initialization options via locale_config hash
45
- return unless opts[:locale_config]
46
-
47
- locale_opts = opts[:locale_config]
48
- if locale_opts[:available_locales] || locale_opts[:available]
49
- @locale_config[:available_locales] =
50
- locale_opts[:available_locales] || locale_opts[:available]
51
- end
52
- return unless locale_opts[:default_locale] || locale_opts[:default]
53
-
54
- @locale_config[:default_locale] =
55
- locale_opts[:default_locale] || locale_opts[:default]
37
+ # Create Otto::Locale::Config instance
38
+ @locale_config = Otto::Locale::Config.new(
39
+ available_locales: available_locales,
40
+ default_locale: default_locale
41
+ )
56
42
  end
57
43
 
58
44
  def configure_security(opts)
@@ -83,10 +69,7 @@ class Otto
83
69
  @auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
84
70
  @auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]
85
71
 
86
- # Enable authentication middleware if strategies are configured
87
- return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
88
-
89
- enable_authentication!
72
+ # No-op: authentication strategies are configured via @auth_config above
90
73
  end
91
74
 
92
75
  def configure_mcp(opts)
@@ -115,9 +98,14 @@ class Otto
115
98
  # default_locale: 'en'
116
99
  # )
117
100
  def configure(available_locales: nil, default_locale: nil)
118
- @locale_config ||= {}
119
- @locale_config[:available_locales] = available_locales if available_locales
120
- @locale_config[:default_locale] = default_locale if default_locale
101
+ ensure_not_frozen!
102
+
103
+ # Initialize locale_config if not already set
104
+ @locale_config ||= Otto::Locale::Config.new
105
+
106
+ # Update configuration
107
+ @locale_config.available_locales = available_locales if available_locales
108
+ @locale_config.default_locale = default_locale if default_locale
121
109
  end
122
110
 
123
111
  # Configure rate limiting settings.
@@ -134,6 +122,7 @@ class Otto
134
122
  # }
135
123
  # })
136
124
  def configure_rate_limiting(config)
125
+ ensure_not_frozen!
137
126
  @security_config.rate_limiting_config.merge!(config)
138
127
  end
139
128
 
@@ -149,18 +138,74 @@ class Otto
149
138
  # 'api_key' => Otto::Security::Authentication::Strategies::APIKeyStrategy.new(api_keys: ['secret123'])
150
139
  # })
151
140
  def configure_auth_strategies(strategies, default_strategy: 'noauth')
141
+ ensure_not_frozen!
152
142
  # Update existing @auth_config rather than creating a new one
153
143
  @auth_config[:auth_strategies] = strategies
154
144
  @auth_config[:default_auth_strategy] = default_strategy
145
+ end
155
146
 
156
- enable_authentication! unless strategies.empty?
147
+ # Freeze the application configuration to prevent runtime modifications.
148
+ # Called automatically at the end of initialization to ensure immutability.
149
+ #
150
+ # This prevents security-critical configuration from being modified after
151
+ # the application begins handling requests. Uses deep freezing to prevent
152
+ # both direct modification and modification through nested structures.
153
+ #
154
+ # @raise [RuntimeError] if configuration is already frozen
155
+ # @return [self]
156
+ def freeze_configuration!
157
+ if frozen_configuration?
158
+ Otto.structured_log(:debug, 'Configuration already frozen', { status: 'skipped' }) if Otto.debug
159
+ return self
160
+ end
161
+
162
+ start_time = Otto::Utils.now_in_μs
163
+
164
+ # Deep freeze configuration objects with memoization support
165
+ @security_config.deep_freeze! if @security_config.respond_to?(:deep_freeze!)
166
+ @locale_config.deep_freeze! if @locale_config.respond_to?(:deep_freeze!)
167
+ @middleware.deep_freeze! if @middleware.respond_to?(:deep_freeze!)
168
+
169
+ # Deep freeze configuration hashes (recursively freezes nested structures)
170
+ deep_freeze_value(@auth_config) if @auth_config
171
+ deep_freeze_value(@option) if @option
172
+
173
+ # Deep freeze route structures (prevent modification of nested hashes/arrays)
174
+ deep_freeze_value(@routes) if @routes
175
+ deep_freeze_value(@routes_literal) if @routes_literal
176
+ deep_freeze_value(@routes_static) if @routes_static
177
+ deep_freeze_value(@route_definitions) if @route_definitions
178
+
179
+ @configuration_frozen = true
180
+
181
+ duration = Otto::Utils.now_in_μs - start_time
182
+ frozen_objects = %w[security_config locale_config middleware auth_config option routes]
183
+ Otto.structured_log(:info, 'Freezing completed',
184
+ {
185
+ duration: duration,
186
+ frozen_objects: frozen_objects.join(','),
187
+ })
188
+
189
+ self
157
190
  end
158
191
 
159
- private
192
+ # Check if configuration is frozen
193
+ #
194
+ # @return [Boolean] true if configuration is frozen
195
+ def frozen_configuration?
196
+ @configuration_frozen == true
197
+ end
198
+
199
+ # Ensure configuration is not frozen before allowing mutations
200
+ #
201
+ # @raise [FrozenError] if configuration is frozen
202
+ def ensure_not_frozen!
203
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen_configuration?
204
+ end
160
205
 
161
206
  def middleware_enabled?(middleware_class)
162
207
  # Only check the new middleware stack as the single source of truth
163
- @middleware && @middleware.includes?(middleware_class)
208
+ @middleware&.includes?(middleware_class)
164
209
  end
165
210
  end
166
211
  end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/error_handler.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'securerandom'
6
6
  require 'json'
@@ -11,10 +11,30 @@ class Otto
11
11
  # Error handling module providing secure error reporting and logging functionality
12
12
  module ErrorHandler
13
13
  def handle_error(error, env)
14
+ # Check if this is a registered expected error
15
+ if handler_config = @error_handlers[error.class.name]
16
+ return handle_expected_error(error, env, handler_config)
17
+ end
18
+
14
19
  # Log error details internally but don't expose them
15
20
  error_id = SecureRandom.hex(8)
16
- Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
17
- Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
21
+
22
+ # Base context pattern: create once, reuse for correlation
23
+ base_context = Otto::LoggingHelpers.request_context(env)
24
+
25
+ # Include handler context if available (set by route handlers)
26
+ log_context = base_context.merge(
27
+ error: error.message,
28
+ error_class: error.class.name,
29
+ error_id: error_id
30
+ )
31
+ log_context[:handler] = env['otto.handler'] if env['otto.handler']
32
+ log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
33
+
34
+ Otto.structured_log(:error, 'Unhandled error in request', log_context)
35
+
36
+ Otto::LoggingHelpers.log_backtrace(error,
37
+ base_context.merge(error_id: error_id))
18
38
 
19
39
  # Parse request for content negotiation
20
40
  begin
@@ -30,7 +50,21 @@ class Otto
30
50
  env['otto.error_id'] = error_id
31
51
  return found_route.call(env)
32
52
  rescue StandardError => e
33
- Otto.logger.error "[#{error_id}] Error in custom error handler: #{e.message}"
53
+ # When the custom error handler itself fails, generate a new error ID
54
+ # to distinguish it from the original error, but link them.
55
+ custom_handler_error_id = SecureRandom.hex(8)
56
+ base_context = Otto::LoggingHelpers.request_context(env)
57
+
58
+ Otto.structured_log(:error, 'Error in custom error handler',
59
+ base_context.merge(
60
+ error: e.message,
61
+ error_class: e.class.name,
62
+ error_id: custom_handler_error_id,
63
+ original_error_id: error_id # Link to original error
64
+ ))
65
+
66
+ Otto::LoggingHelpers.log_backtrace(e,
67
+ base_context.merge(error_id: custom_handler_error_id, original_error_id: error_id))
34
68
  end
35
69
  end
36
70
 
@@ -44,6 +78,102 @@ class Otto
44
78
 
45
79
  private
46
80
 
81
+ # Handle expected business logic errors with custom status codes and logging
82
+ #
83
+ # @param error [Exception] The expected error to handle
84
+ # @param env [Hash] Rack environment hash
85
+ # @param handler_config [Hash] Configuration from error_handlers registry
86
+ # @return [Array] Rack response tuple [status, headers, body]
87
+ def handle_expected_error(error, env, handler_config)
88
+ # Generate error ID for correlation (even for expected errors)
89
+ error_id = SecureRandom.hex(8)
90
+
91
+ # Base context pattern: create once, reuse for correlation
92
+ base_context = Otto::LoggingHelpers.request_context(env)
93
+
94
+ # Include handler context if available
95
+ log_context = base_context.merge(
96
+ error: error.message,
97
+ error_class: error.class.name,
98
+ error_id: error_id,
99
+ expected: true # Mark as expected error
100
+ )
101
+ log_context[:handler] = env['otto.handler'] if env['otto.handler']
102
+ log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
103
+
104
+ # Log at configured level (info/warn instead of error)
105
+ log_level = handler_config[:log_level] || :info
106
+ Otto.structured_log(log_level, 'Expected error in request', log_context)
107
+
108
+ # Build response body
109
+ response_body = if handler_config[:handler]
110
+ # Use custom handler block if provided
111
+ begin
112
+ req = Rack::Request.new(env)
113
+ result = handler_config[:handler].call(error, req)
114
+
115
+ # Validate that custom handler returned a Hash
116
+ unless result.is_a?(Hash)
117
+ base_context = Otto::LoggingHelpers.request_context(env)
118
+ Otto.structured_log(:warn, 'Custom error handler returned non-hash value',
119
+ base_context.merge(
120
+ error_class: error.class.name,
121
+ handler_result_class: result.class.name,
122
+ error_id: error_id
123
+ ))
124
+ result = { error: error.class.name.split('::').last, message: error.message }
125
+ end
126
+
127
+ result
128
+ rescue StandardError => e
129
+ # If custom handler fails, fall back to default
130
+ base_context = Otto::LoggingHelpers.request_context(env)
131
+ Otto.structured_log(:warn, 'Error in custom error handler',
132
+ base_context.merge(
133
+ error: e.message,
134
+ error_class: e.class.name,
135
+ original_error_class: error.class.name,
136
+ error_id: error_id
137
+ ))
138
+ { error: error.class.name.split('::').last, message: error.message }
139
+ end
140
+ else
141
+ # Default response body
142
+ { error: error.class.name.split('::').last, message: error.message }
143
+ end
144
+
145
+ # Add error_id in development mode
146
+ response_body[:error_id] = error_id if Otto.env?(:dev, :development)
147
+
148
+ # Content negotiation
149
+ accept_header = env['HTTP_ACCEPT'].to_s
150
+ status = handler_config[:status] || 500
151
+
152
+ if accept_header.include?('application/json')
153
+ body = JSON.generate(response_body)
154
+ headers = {
155
+ 'content-type' => 'application/json',
156
+ 'content-length' => body.bytesize.to_s
157
+ }.merge(@security_config.security_headers)
158
+
159
+ [status, headers, [body]]
160
+ else
161
+ # Plain text response
162
+ body = if Otto.env?(:dev, :development)
163
+ "#{response_body[:error]}: #{response_body[:message]} (ID: #{error_id})"
164
+ else
165
+ "#{response_body[:error]}: #{response_body[:message]}"
166
+ end
167
+
168
+ headers = {
169
+ 'content-type' => 'text/plain',
170
+ 'content-length' => body.bytesize.to_s
171
+ }.merge(@security_config.security_headers)
172
+
173
+ [status, headers, [body]]
174
+ end
175
+ end
176
+
47
177
  def secure_error_response(error_id)
48
178
  body = if Otto.env?(:dev, :development)
49
179
  "Server error (ID: #{error_id}). Check logs for details."
@@ -62,13 +192,13 @@ class Otto
62
192
  def json_error_response(error_id)
63
193
  error_data = if Otto.env?(:dev, :development)
64
194
  {
65
- error: 'Internal Server Error',
66
- message: 'Server error occurred. Check logs for details.',
195
+ error: 'Internal Server Error',
196
+ message: 'Server error occurred. Check logs for details.',
67
197
  error_id: error_id,
68
198
  }
69
199
  else
70
200
  {
71
- error: 'Internal Server Error',
201
+ error: 'Internal Server Error',
72
202
  message: 'An error occurred. Please try again later.',
73
203
  }
74
204
  end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/file_safety.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  module Core
@@ -0,0 +1,93 @@
1
+ # lib/otto/core/freezable.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require 'set'
6
+
7
+ class Otto
8
+ module Core
9
+ # Provides deep freezing capability for configuration objects
10
+ #
11
+ # This module enables objects to be deeply frozen, preventing any
12
+ # modifications to the object itself and all its nested structures.
13
+ # This is critical for security as it prevents runtime tampering with
14
+ # security configurations.
15
+ #
16
+ # @example
17
+ # class MyConfig
18
+ # include Otto::Core::Freezable
19
+ #
20
+ # def initialize
21
+ # @settings = { security: { enabled: true } }
22
+ # end
23
+ # end
24
+ #
25
+ # config = MyConfig.new
26
+ # config.deep_freeze!
27
+ # # Now config and all nested hashes/arrays are frozen
28
+ #
29
+ module Freezable
30
+ # Deeply freeze this object and all its instance variables
31
+ #
32
+ # This method recursively freezes all nested structures including:
33
+ # - Hashes (both keys and values)
34
+ # - Arrays (and all elements)
35
+ # - Sets
36
+ # - Other freezable objects
37
+ #
38
+ # NOTE: This method is idempotent and safe to call multiple times.
39
+ #
40
+ # @return [self] The frozen object
41
+ def deep_freeze!
42
+ return self if frozen?
43
+
44
+ freeze_instance_variables!
45
+ freeze
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ # Freeze all instance variables recursively
52
+ def freeze_instance_variables!
53
+ instance_variables.each do |var|
54
+ value = instance_variable_get(var)
55
+ deep_freeze_value(value)
56
+ end
57
+ end
58
+
59
+ # Recursively freeze a value based on its type
60
+ #
61
+ # @param value [Object] Value to freeze
62
+ # @return [void]
63
+ def deep_freeze_value(value)
64
+ case value
65
+ when Hash
66
+ # Freeze hash keys and values, then freeze the hash itself
67
+ value.each do |k, v|
68
+ k.freeze unless k.frozen?
69
+ deep_freeze_value(v)
70
+ end
71
+ value.freeze
72
+ when Array
73
+ # Freeze all array elements, then freeze the array
74
+ value.each { |item| deep_freeze_value(item) }
75
+ value.freeze
76
+ when Set
77
+ # Sets are immutable once frozen
78
+ value.freeze
79
+ when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
80
+ # These types are either immutable or already frozen
81
+ value.freeze if value.respond_to?(:freeze) && !value.frozen?
82
+ else
83
+ # For other objects, recursively freeze if they support it, otherwise shallow freeze.
84
+ if value.respond_to?(:deep_freeze!)
85
+ value.deep_freeze!
86
+ elsif value.respond_to?(:freeze) && !value.frozen?
87
+ value.freeze
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,6 +1,8 @@
1
+ # lib/otto/core/middleware_stack.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
- # lib/otto/core/middleware_stack.rb
5
+ require_relative 'freezable'
4
6
 
5
7
  class Otto
6
8
  module Core
@@ -9,10 +11,18 @@ class Otto
9
11
  # and improved execution chain management.
10
12
  class MiddlewareStack
11
13
  include Enumerable
14
+ include Otto::Core::Freezable
12
15
 
13
16
  def initialize
14
17
  @stack = []
15
18
  @middleware_set = Set.new
19
+ @on_change_callback = nil
20
+ end
21
+
22
+ # Set a callback to be invoked when the middleware stack changes
23
+ # @param callback [Proc] A callable object (e.g., method or lambda)
24
+ def on_change(&callback)
25
+ @on_change_callback = callback
16
26
  end
17
27
 
18
28
  # Enhanced middleware registration with argument uniqueness and immutability check
@@ -33,8 +43,8 @@ class Otto
33
43
  entry = { middleware: middleware_class, args: args, options: options }
34
44
  @stack << entry
35
45
  @middleware_set.add(middleware_class)
36
- # Invalidate memoized middleware list
37
- @memoized_middleware_list = nil
46
+ # Notify of change
47
+ @on_change_callback&.call
38
48
  end
39
49
 
40
50
  # Add middleware with position hint for optimal ordering
@@ -67,7 +77,8 @@ class Otto
67
77
  end
68
78
 
69
79
  @middleware_set.add(middleware_class)
70
- @memoized_middleware_list = nil
80
+ # Notify of change
81
+ @on_change_callback&.call
71
82
  end
72
83
 
73
84
  # Validate MCP middleware ordering
@@ -131,8 +142,8 @@ class Otto
131
142
 
132
143
  # Rebuild the set of unique middleware classes
133
144
  @middleware_set = Set.new(@stack.map { |entry| entry[:middleware] })
134
- # Invalidate memoized middleware list
135
- @memoized_middleware_list = nil
145
+ # Notify of change
146
+ @on_change_callback&.call
136
147
  end
137
148
 
138
149
  # Check if middleware is registered - now O(1) using Set
@@ -147,8 +158,8 @@ class Otto
147
158
 
148
159
  @stack.clear
149
160
  @middleware_set.clear
150
- # Invalidate memoized middleware list
151
- @memoized_middleware_list = nil
161
+ # Notify of change
162
+ @on_change_callback&.call
152
163
  end
153
164
 
154
165
  # Enumerable support
@@ -157,7 +168,7 @@ class Otto
157
168
  end
158
169
 
159
170
  # Build Rack application with middleware chain
160
- def build_app(base_app, security_config = nil)
171
+ def wrap(base_app, security_config = nil)
161
172
  @stack.reduce(base_app) do |app, entry|
162
173
  middleware = entry[:middleware]
163
174
  args = entry[:args]
@@ -177,10 +188,9 @@ class Otto
177
188
  end
178
189
  end
179
190
 
180
- # Cached middleware list to reduce array creation
191
+ # Returns list of middleware classes in order
181
192
  def middleware_list
182
- # Memoize the result to avoid repeated array creation
183
- @memoized_middleware_list ||= @stack.map { |entry| entry[:middleware] }
193
+ @stack.map { |entry| entry[:middleware] }
184
194
  end
185
195
 
186
196
  # Detailed introspection
@@ -215,6 +225,8 @@ class Otto
215
225
  @stack.reverse_each(&)
216
226
  end
217
227
 
228
+
229
+
218
230
  private
219
231
 
220
232
  def middleware_needs_config?(middleware_class)
@@ -224,12 +236,7 @@ class Otto
224
236
  Otto::Security::Middleware::CSRFMiddleware,
225
237
  Otto::Security::Middleware::ValidationMiddleware,
226
238
  Otto::Security::Middleware::RateLimitMiddleware,
227
- Otto::Security::Authentication::AuthenticationMiddleware,
228
- # Backward compatibility aliases
229
- Otto::Security::CSRFMiddleware,
230
- Otto::Security::ValidationMiddleware,
231
- Otto::Security::RateLimitMiddleware,
232
- Otto::Security::AuthenticationMiddleware,
239
+ Otto::Security::Middleware::IPPrivacyMiddleware,
233
240
  ].include?(middleware_class)
234
241
  end
235
242
  end