otto 2.0.0.pre1 → 2.0.0.pre3

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -3
  3. data/.github/workflows/claude-code-review.yml +30 -14
  4. data/.github/workflows/claude.yml +1 -1
  5. data/.rubocop.yml +4 -1
  6. data/CHANGELOG.rst +54 -6
  7. data/CLAUDE.md +537 -0
  8. data/Gemfile +3 -2
  9. data/Gemfile.lock +34 -26
  10. data/benchmark_middleware_wrap.rb +163 -0
  11. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
  12. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -0
  13. data/docs/.gitignore +2 -0
  14. data/docs/ipaddr-encoding-quirk.md +34 -0
  15. data/docs/migrating/v2.0.0-pre2.md +338 -0
  16. data/examples/authentication_strategies/config.ru +0 -1
  17. data/lib/otto/core/configuration.rb +91 -41
  18. data/lib/otto/core/freezable.rb +93 -0
  19. data/lib/otto/core/middleware_stack.rb +103 -16
  20. data/lib/otto/core/router.rb +8 -7
  21. data/lib/otto/core.rb +8 -0
  22. data/lib/otto/env_keys.rb +118 -0
  23. data/lib/otto/helpers/base.rb +2 -21
  24. data/lib/otto/helpers/request.rb +80 -2
  25. data/lib/otto/helpers/response.rb +25 -3
  26. data/lib/otto/helpers.rb +4 -0
  27. data/lib/otto/locale/config.rb +56 -0
  28. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +3 -2
  29. data/lib/otto/mcp/server.rb +26 -13
  30. data/lib/otto/mcp.rb +3 -0
  31. data/lib/otto/privacy/config.rb +199 -0
  32. data/lib/otto/privacy/geo_resolver.rb +115 -0
  33. data/lib/otto/privacy/ip_privacy.rb +175 -0
  34. data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
  35. data/lib/otto/privacy.rb +29 -0
  36. data/lib/otto/response_handlers/json.rb +6 -0
  37. data/lib/otto/route.rb +44 -48
  38. data/lib/otto/route_handlers/base.rb +1 -2
  39. data/lib/otto/route_handlers/factory.rb +24 -9
  40. data/lib/otto/route_handlers/logic_class.rb +2 -2
  41. data/lib/otto/security/authentication/auth_failure.rb +44 -0
  42. data/lib/otto/security/authentication/auth_strategy.rb +3 -3
  43. data/lib/otto/security/authentication/route_auth_wrapper.rb +260 -0
  44. data/lib/otto/security/authentication/strategies/{public_strategy.rb → noauth_strategy.rb} +6 -2
  45. data/lib/otto/security/authentication/strategy_result.rb +129 -15
  46. data/lib/otto/security/authentication.rb +5 -6
  47. data/lib/otto/security/config.rb +51 -18
  48. data/lib/otto/security/configurator.rb +2 -15
  49. data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
  50. data/lib/otto/security/middleware/rate_limit_middleware.rb +19 -3
  51. data/lib/otto/security.rb +9 -0
  52. data/lib/otto/version.rb +1 -1
  53. data/lib/otto.rb +183 -89
  54. data/otto.gemspec +5 -0
  55. metadata +83 -8
  56. data/changelog.d/20250911_235619_delano_next.rst +0 -28
  57. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +0 -21
  58. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +0 -21
  59. data/lib/otto/security/authentication/authentication_middleware.rb +0 -123
  60. data/lib/otto/security/authentication/failure_result.rb +0 -36
data/lib/otto.rb CHANGED
@@ -11,29 +11,19 @@ require 'rack/request'
11
11
  require 'rack/response'
12
12
  require 'rack/utils'
13
13
 
14
- require_relative 'otto/security/authentication/strategy_result'
15
14
  require_relative 'otto/route_definition'
16
15
  require_relative 'otto/route'
17
16
  require_relative 'otto/static'
18
- require_relative 'otto/helpers/request'
19
- require_relative 'otto/helpers/response'
17
+ require_relative 'otto/helpers'
20
18
  require_relative 'otto/response_handlers'
21
19
  require_relative 'otto/route_handlers'
22
- require_relative 'otto/version'
23
- require_relative 'otto/security/config'
24
- require_relative 'otto/security/middleware/csrf_middleware'
25
- require_relative 'otto/security/middleware/validation_middleware'
26
- require_relative 'otto/security/authentication/authentication_middleware'
27
- require_relative 'otto/security/middleware/rate_limit_middleware'
28
- require_relative 'otto/mcp/server'
29
- require_relative 'otto/core/router'
30
- require_relative 'otto/core/file_safety'
31
- require_relative 'otto/core/configuration'
32
- require_relative 'otto/core/error_handler'
33
- require_relative 'otto/core/uri_generator'
34
- require_relative 'otto/core/middleware_stack'
35
- require_relative 'otto/security/configurator'
20
+ require_relative 'otto/locale/config'
21
+ require_relative 'otto/mcp'
22
+ require_relative 'otto/core'
23
+ require_relative 'otto/privacy'
24
+ require_relative 'otto/security'
36
25
  require_relative 'otto/utils'
26
+ require_relative 'otto/version'
37
27
 
38
28
  # Otto is a simple Rack router that allows you to define routes in a file
39
29
  # with built-in security features including CSRF protection, input validation,
@@ -56,38 +46,6 @@ require_relative 'otto/utils'
56
46
  # otto.enable_csp!
57
47
  # otto.enable_frame_protection!
58
48
  #
59
- # Configuration Data class to replace OpenStruct
60
- # Configuration Data class to replace OpenStruct
61
- # Configuration class to replace OpenStruct
62
- class ConfigData
63
- def initialize(**kwargs)
64
- @data = kwargs
65
- end
66
-
67
- # Dynamic attribute accessors
68
- def method_missing(method_name, *args)
69
- if method_name.to_s.end_with?('=')
70
- # Setter
71
- attr_name = method_name.to_s.chomp('=').to_sym
72
- @data[attr_name] = args.first
73
- elsif @data.key?(method_name)
74
- # Getter
75
- @data[method_name]
76
- else
77
- super
78
- end
79
- end
80
-
81
- def respond_to_missing?(method_name, include_private = false)
82
- method_name.to_s.end_with?('=') || @data.key?(method_name) || super
83
- end
84
-
85
- # Convert to hash for compatibility
86
- def to_h
87
- @data.dup
88
- end
89
- end
90
-
91
49
  class Otto
92
50
  include Otto::Core::Router
93
51
  include Otto::Core::FileSafety
@@ -103,25 +61,11 @@ class Otto
103
61
  else
104
62
  defined?(Otto::Utils) ? Otto::Utils.yes?(ENV.fetch('OTTO_DEBUG', nil)) : false
105
63
  end
106
- @logger = Logger.new($stdout, Logger::INFO)
107
- @global_config = nil
108
-
109
- # Global configuration for all Otto instances (Ruby 3.2+ pattern matching)
110
- def self.configure
111
- config = case @global_config
112
- in Hash => h
113
- # Transform string keys to symbol keys for ConfigData compatibility
114
- symbol_hash = h.transform_keys(&:to_sym)
115
- ConfigData.new(**symbol_hash)
116
- else
117
- ConfigData.new
118
- end
119
- yield config
120
- @global_config = config.to_h
121
- end
64
+ @logger = Logger.new($stdout, Logger::INFO)
122
65
 
123
- attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route,
124
- :security_config, :locale_config, :auth_config, :route_handler_factory, :mcp_server, :security, :middleware
66
+ attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option,
67
+ :static_route, :security_config, :locale_config, :auth_config,
68
+ :route_handler_factory, :mcp_server, :security, :middleware
125
69
  attr_accessor :not_found, :server_error
126
70
 
127
71
  def initialize(path = nil, opts = {})
@@ -132,27 +76,76 @@ class Otto
132
76
  Otto.logger.debug "new Otto: #{opts}" if Otto.debug
133
77
  load(path) unless path.nil?
134
78
  super()
79
+
80
+ # Build the middleware app once after all initialization is complete
81
+ build_app!
82
+
83
+ # Configuration freezing is deferred until first request to support
84
+ # multi-step initialization (e.g., multi-app architectures).
85
+ # This allows adding auth strategies, middleware, etc. after Otto.new
86
+ # but before processing requests.
87
+ @freeze_mutex = Mutex.new
88
+ @configuration_frozen = false
135
89
  end
136
90
  alias options option
137
91
 
138
92
  # Main Rack application interface
139
93
  def call(env)
140
- # Apply middleware stack
141
- base_app = ->(e) { handle_request(e) }
142
-
143
- # Use the middleware stack as the source of truth
144
- app = @middleware.build_app(base_app, @security_config)
94
+ # Freeze configuration on first request (thread-safe)
95
+ # Skip in test environment to allow test flexibility
96
+ unless defined?(RSpec) || @configuration_frozen
97
+ Otto.logger.debug '[Otto] Lazy freezing check: configuration not yet frozen' if Otto.debug
98
+
99
+ @freeze_mutex.synchronize do
100
+ unless @configuration_frozen
101
+ Otto.logger.info '[Otto] Freezing configuration on first request (lazy freeze)'
102
+ freeze_configuration!
103
+ @configuration_frozen = true
104
+ Otto.logger.debug '[Otto] Configuration frozen successfully' if Otto.debug
105
+ end
106
+ end
107
+ end
145
108
 
146
109
  begin
147
- app.call(env)
110
+ # Use pre-built middleware app (built once at initialization)
111
+ @app.call(env)
148
112
  rescue StandardError => e
149
113
  handle_error(e, env)
150
114
  end
151
115
  end
152
116
 
117
+ # Builds the middleware application chain
118
+ # Called once at initialization and whenever middleware stack changes
119
+ #
120
+ # IMPORTANT: If you have routes with auth requirements, you MUST add session
121
+ # middleware to your middleware stack BEFORE Otto processes requests.
122
+ #
123
+ # Session middleware is required for RouteAuthWrapper to correctly persist
124
+ # session changes during authentication. Common options include:
125
+ # - Rack::Session::Cookie (requires rack-session gem)
126
+ # - Rack::Session::Pool
127
+ # - Rack::Session::Memcache
128
+ # - Any Rack-compatible session middleware
129
+ #
130
+ # Example:
131
+ # use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
132
+ # otto = Otto.new('routes.txt')
133
+ #
134
+ def build_app!
135
+ base_app = method(:handle_request)
136
+ @app = @middleware.wrap(base_app, @security_config)
137
+ end
138
+
153
139
  # Middleware Management
154
140
  def use(middleware, ...)
141
+ ensure_not_frozen!
155
142
  @middleware.add(middleware, ...)
143
+
144
+ # NOTE: If build_app! is triggered during a request (via use() or
145
+ # middleware_stack=), the @app instance variable could be swapped
146
+ # mid-request in a multi-threaded environment.
147
+
148
+ build_app! if @app # Rebuild app if already initialized
156
149
  end
157
150
 
158
151
  # Compatibility method for existing tests
@@ -164,6 +157,7 @@ class Otto
164
157
  def middleware_stack=(stack)
165
158
  @middleware.clear!
166
159
  Array(stack).each { |middleware| @middleware.add(middleware) }
160
+ build_app! if @app # Rebuild app if already initialized
167
161
  end
168
162
 
169
163
  # Compatibility method for middleware detection
@@ -180,6 +174,7 @@ class Otto
180
174
  # @example
181
175
  # otto.enable_csrf_protection!
182
176
  def enable_csrf_protection!
177
+ ensure_not_frozen!
183
178
  return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
184
179
 
185
180
  @security_config.enable_csrf_protection!
@@ -192,6 +187,7 @@ class Otto
192
187
  # @example
193
188
  # otto.enable_request_validation!
194
189
  def enable_request_validation!
190
+ ensure_not_frozen!
195
191
  return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
196
192
 
197
193
  @security_config.input_validation = true
@@ -207,6 +203,7 @@ class Otto
207
203
  # @example
208
204
  # otto.enable_rate_limiting!(requests_per_minute: 50)
209
205
  def enable_rate_limiting!(options = {})
206
+ ensure_not_frozen!
210
207
  return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
211
208
 
212
209
  @security.configure_rate_limiting(options)
@@ -223,7 +220,7 @@ class Otto
223
220
  # @example
224
221
  # otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
225
222
  def add_rate_limit_rule(name, options)
226
- @security_config.rate_limiting_config[:custom_rules] ||= {}
223
+ ensure_not_frozen!
227
224
  @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
228
225
  end
229
226
 
@@ -235,6 +232,7 @@ class Otto
235
232
  # otto.add_trusted_proxy('10.0.0.0/8')
236
233
  # otto.add_trusted_proxy(/^172\.16\./)
237
234
  def add_trusted_proxy(proxy)
235
+ ensure_not_frozen!
238
236
  @security_config.add_trusted_proxy(proxy)
239
237
  end
240
238
 
@@ -248,6 +246,7 @@ class Otto
248
246
  # 'strict-transport-security' => 'max-age=31536000'
249
247
  # })
250
248
  def set_security_headers(headers)
249
+ ensure_not_frozen!
251
250
  @security_config.security_headers.merge!(headers)
252
251
  end
253
252
 
@@ -260,6 +259,7 @@ class Otto
260
259
  # @example
261
260
  # otto.enable_hsts!(max_age: 86400, include_subdomains: false)
262
261
  def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
262
+ ensure_not_frozen!
263
263
  @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
264
264
  end
265
265
 
@@ -270,6 +270,7 @@ class Otto
270
270
  # @example
271
271
  # otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
272
272
  def enable_csp!(policy = "default-src 'self'")
273
+ ensure_not_frozen!
273
274
  @security_config.enable_csp!(policy)
274
275
  end
275
276
 
@@ -279,6 +280,7 @@ class Otto
279
280
  # @example
280
281
  # otto.enable_frame_protection!('DENY')
281
282
  def enable_frame_protection!(option = 'SAMEORIGIN')
283
+ ensure_not_frozen!
282
284
  @security_config.enable_frame_protection!(option)
283
285
  end
284
286
 
@@ -289,20 +291,10 @@ class Otto
289
291
  # @example
290
292
  # otto.enable_csp_with_nonce!(debug: true)
291
293
  def enable_csp_with_nonce!(debug: false)
294
+ ensure_not_frozen!
292
295
  @security_config.enable_csp_with_nonce!(debug: debug)
293
296
  end
294
297
 
295
- # Enable authentication middleware for route-level access control.
296
- # This will automatically check route auth parameters and enforce authentication.
297
- #
298
- # @example
299
- # otto.enable_authentication!
300
- def enable_authentication!
301
- return if @middleware.includes?(Otto::Security::Authentication::AuthenticationMiddleware)
302
-
303
- use Otto::Security::Authentication::AuthenticationMiddleware, @auth_config
304
- end
305
-
306
298
  # Add a single authentication strategy
307
299
  #
308
300
  # @param name [String] Strategy name
@@ -310,12 +302,83 @@ class Otto
310
302
  # @example
311
303
  # otto.add_auth_strategy('custom', MyCustomStrategy.new)
312
304
  def add_auth_strategy(name, strategy)
305
+ ensure_not_frozen!
313
306
  # Ensure auth_config is initialized (handles edge case where it might be nil)
314
- @auth_config = { auth_strategies: {}, default_auth_strategy: 'publicly' } if @auth_config.nil?
307
+ @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
315
308
 
316
309
  @auth_config[:auth_strategies][name] = strategy
310
+ end
317
311
 
318
- enable_authentication!
312
+ # Disable IP privacy to access original IP addresses
313
+ #
314
+ # IMPORTANT: By default, Otto masks public IP addresses for privacy.
315
+ # Private/localhost IPs (127.0.0.0/8, 10.0.0.0/8, etc.) are never masked.
316
+ # Only disable this if you need access to original public IPs.
317
+ #
318
+ # When disabled:
319
+ # - env['REMOTE_ADDR'] contains the real IP address
320
+ # - env['otto.original_ip'] also contains the real IP
321
+ # - No PrivateFingerprint is created
322
+ #
323
+ # @example
324
+ # otto.disable_ip_privacy!
325
+ def disable_ip_privacy!
326
+ ensure_not_frozen!
327
+ @security_config.ip_privacy_config.disable!
328
+ end
329
+
330
+
331
+ # Enable full IP privacy (mask ALL IPs including private/localhost)
332
+ #
333
+ # By default, Otto exempts private and localhost IPs from masking for
334
+ # better development experience. Call this method to mask ALL IPs
335
+ # regardless of type.
336
+ #
337
+ # @example Enable full privacy (mask all IPs)
338
+ # otto = Otto.new(routes_file)
339
+ # otto.enable_full_ip_privacy!
340
+ # # Now 127.0.0.1 → 127.0.0.0, 192.168.1.100 → 192.168.1.0
341
+ #
342
+ # @return [void]
343
+ # @raise [FrozenError] if called after configuration is frozen
344
+ def enable_full_ip_privacy!
345
+ ensure_not_frozen!
346
+ @security_config.ip_privacy_config.mask_private_ips = true
347
+ end
348
+
349
+ # Configure IP privacy settings
350
+ #
351
+ # Privacy is enabled by default. Use this method to customize privacy
352
+ # behavior without disabling it entirely.
353
+ #
354
+ # @param octet_precision [Integer] Number of octets to mask (1 or 2, default: 1)
355
+ # @param hash_rotation [Integer] Seconds between key rotation (default: 86400)
356
+ # @param geo [Boolean] Enable geo-location resolution (default: true)
357
+ # @param redis [Redis] Redis connection for multi-server atomic key generation
358
+ #
359
+ # @example Mask 2 octets instead of 1
360
+ # otto.configure_ip_privacy(octet_precision: 2)
361
+ #
362
+ # @example Disable geo-location
363
+ # otto.configure_ip_privacy(geo: false)
364
+ #
365
+ # @example Custom hash rotation
366
+ # otto.configure_ip_privacy(hash_rotation: 24.hours)
367
+ #
368
+ # @example Multi-server with Redis
369
+ # redis = Redis.new(url: ENV['REDIS_URL'])
370
+ # otto.configure_ip_privacy(redis: redis)
371
+ def configure_ip_privacy(octet_precision: nil, hash_rotation: nil, geo: nil, redis: nil)
372
+ ensure_not_frozen!
373
+ config = @security_config.ip_privacy_config
374
+
375
+ config.octet_precision = octet_precision if octet_precision
376
+ config.hash_rotation_period = hash_rotation if hash_rotation
377
+ config.geo_enabled = geo unless geo.nil?
378
+ config.instance_variable_set(:@redis, redis) if redis
379
+
380
+ # Validate configuration
381
+ config.validate!
319
382
  end
320
383
 
321
384
  # Enable MCP (Model Context Protocol) server support
@@ -327,6 +390,7 @@ class Otto
327
390
  # @example
328
391
  # otto.enable_mcp!(http: true, endpoint: '/api/mcp')
329
392
  def enable_mcp!(options = {})
393
+ ensure_not_frozen!
330
394
  @mcp_server ||= Otto::MCP::Server.new(self)
331
395
 
332
396
  @mcp_server.enable!(options)
@@ -349,8 +413,16 @@ class Otto
349
413
  @security_config = Otto::Security::Config.new
350
414
  @middleware = Otto::Core::MiddlewareStack.new
351
415
  # Initialize @auth_config first so it can be shared with the configurator
352
- @auth_config = { auth_strategies: {}, default_auth_strategy: 'publicly' }
416
+ @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' }
353
417
  @security = Otto::Security::Configurator.new(@security_config, @middleware, @auth_config)
418
+ @app = nil # Pre-built middleware app (built after initialization)
419
+
420
+ # Add IP Privacy middleware first in stack (privacy by default for public IPs)
421
+ # Private/localhost IPs are automatically exempted from masking
422
+ @middleware.add_with_position(
423
+ Otto::Security::Middleware::IPPrivacyMiddleware,
424
+ position: :first
425
+ )
354
426
  end
355
427
 
356
428
  def initialize_options(_path, opts)
@@ -376,7 +448,7 @@ class Otto
376
448
  end
377
449
 
378
450
  class << self
379
- attr_accessor :debug, :logger, :global_config # rubocop:disable ThreadSafety/ClassAndModuleAttributes
451
+ attr_accessor :debug, :logger # rubocop:disable ThreadSafety/ClassAndModuleAttributes
380
452
  end
381
453
 
382
454
  # Class methods for Otto framework providing singleton access and configuration
@@ -401,6 +473,28 @@ class Otto
401
473
  def env? *guesses
402
474
  !guesses.flatten.select { |n| ENV['RACK_ENV'].to_s == n.to_s }.empty?
403
475
  end
476
+
477
+ # Test-only method to unfreeze Otto configuration
478
+ #
479
+ # This method resets the @configuration_frozen flag, allowing tests
480
+ # to bypass the ensure_not_frozen! check. It does NOT actually unfreeze
481
+ # Ruby objects (which is impossible once frozen).
482
+ #
483
+ # IMPORTANT: Only works when RSpec is defined. Raises an error otherwise
484
+ # to prevent accidental use in production.
485
+ #
486
+ # @param otto [Otto] The Otto instance to unfreeze
487
+ # @return [Otto] The unfrozen Otto instance
488
+ # @raise [RuntimeError] if RSpec is not defined (not in test environment)
489
+ # @api private
490
+ def unfreeze_for_testing(otto)
491
+ unless defined?(RSpec)
492
+ raise 'Otto.unfreeze_for_testing is only available in RSpec test environment'
493
+ end
494
+
495
+ otto.instance_variable_set(:@configuration_frozen, false)
496
+ otto
497
+ end
404
498
  end
405
499
  extend ClassMethods
406
500
  end
data/otto.gemspec CHANGED
@@ -16,6 +16,11 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.required_ruby_version = ['>= 3.2', '< 4.0']
18
18
 
19
+ spec.add_dependency 'ipaddr', '~> 1', '< 2.0'
20
+ spec.add_dependency 'concurrent-ruby', '~> 1.3', '< 2.0'
21
+
22
+ # Logger is not part of the default gems as of Ruby 3.5.0
23
+ spec.add_dependency 'logger', '~> 1', '< 2.0'
19
24
 
20
25
  spec.add_dependency 'rack', '~> 3.1', '< 4.0'
21
26
  spec.add_dependency 'rack-parser', '~> 0.7'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otto
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre1
4
+ version: 2.0.0.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -9,6 +9,66 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ipaddr
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: concurrent-ruby
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.3'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.3'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: logger
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '1'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '2.0'
12
72
  - !ruby/object:Gem::Dependency
13
73
  name: rack
14
74
  requirement: !ruby/object:Gem::Requirement
@@ -106,14 +166,16 @@ files:
106
166
  - Gemfile.lock
107
167
  - LICENSE.txt
108
168
  - README.md
169
+ - benchmark_middleware_wrap.rb
109
170
  - bin/rspec
110
- - changelog.d/20250911_235619_delano_next.rst
111
- - changelog.d/20250912_123055_delano_remove_ostruct.rst
112
- - changelog.d/20250912_175625_claude_delano_remove_ostruct.rst
171
+ - changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst
172
+ - changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst
113
173
  - changelog.d/README.md
114
174
  - changelog.d/scriv.ini
115
175
  - docs/.gitignore
176
+ - docs/ipaddr-encoding-quirk.md
116
177
  - docs/migrating/v2.0.0-pre1.md
178
+ - docs/migrating/v2.0.0-pre2.md
117
179
  - examples/.gitignore
118
180
  - examples/advanced_routes/README.md
119
181
  - examples/advanced_routes/app.rb
@@ -168,24 +230,35 @@ files:
168
230
  - examples/security_features/config.ru
169
231
  - examples/security_features/routes
170
232
  - lib/otto.rb
233
+ - lib/otto/core.rb
171
234
  - lib/otto/core/configuration.rb
172
235
  - lib/otto/core/error_handler.rb
173
236
  - lib/otto/core/file_safety.rb
237
+ - lib/otto/core/freezable.rb
174
238
  - lib/otto/core/middleware_stack.rb
175
239
  - lib/otto/core/router.rb
176
240
  - lib/otto/core/uri_generator.rb
177
241
  - lib/otto/design_system.rb
242
+ - lib/otto/env_keys.rb
243
+ - lib/otto/helpers.rb
178
244
  - lib/otto/helpers/base.rb
179
245
  - lib/otto/helpers/request.rb
180
246
  - lib/otto/helpers/response.rb
181
247
  - lib/otto/helpers/validation.rb
248
+ - lib/otto/locale/config.rb
249
+ - lib/otto/mcp.rb
182
250
  - lib/otto/mcp/auth/token.rb
183
251
  - lib/otto/mcp/protocol.rb
184
252
  - lib/otto/mcp/rate_limiting.rb
185
253
  - lib/otto/mcp/registry.rb
186
254
  - lib/otto/mcp/route_parser.rb
255
+ - lib/otto/mcp/schema_validation.rb
187
256
  - lib/otto/mcp/server.rb
188
- - lib/otto/mcp/validation.rb
257
+ - lib/otto/privacy.rb
258
+ - lib/otto/privacy/config.rb
259
+ - lib/otto/privacy/geo_resolver.rb
260
+ - lib/otto/privacy/ip_privacy.rb
261
+ - lib/otto/privacy/redacted_fingerprint.rb
189
262
  - lib/otto/response_handlers.rb
190
263
  - lib/otto/response_handlers/auto.rb
191
264
  - lib/otto/response_handlers/base.rb
@@ -203,13 +276,14 @@ files:
203
276
  - lib/otto/route_handlers/instance_method.rb
204
277
  - lib/otto/route_handlers/lambda.rb
205
278
  - lib/otto/route_handlers/logic_class.rb
279
+ - lib/otto/security.rb
206
280
  - lib/otto/security/authentication.rb
281
+ - lib/otto/security/authentication/auth_failure.rb
207
282
  - lib/otto/security/authentication/auth_strategy.rb
208
- - lib/otto/security/authentication/authentication_middleware.rb
209
- - lib/otto/security/authentication/failure_result.rb
283
+ - lib/otto/security/authentication/route_auth_wrapper.rb
210
284
  - lib/otto/security/authentication/strategies/api_key_strategy.rb
285
+ - lib/otto/security/authentication/strategies/noauth_strategy.rb
211
286
  - lib/otto/security/authentication/strategies/permission_strategy.rb
212
- - lib/otto/security/authentication/strategies/public_strategy.rb
213
287
  - lib/otto/security/authentication/strategies/role_strategy.rb
214
288
  - lib/otto/security/authentication/strategies/session_strategy.rb
215
289
  - lib/otto/security/authentication/strategy_result.rb
@@ -217,6 +291,7 @@ files:
217
291
  - lib/otto/security/configurator.rb
218
292
  - lib/otto/security/csrf.rb
219
293
  - lib/otto/security/middleware/csrf_middleware.rb
294
+ - lib/otto/security/middleware/ip_privacy_middleware.rb
220
295
  - lib/otto/security/middleware/rate_limit_middleware.rb
221
296
  - lib/otto/security/middleware/validation_middleware.rb
222
297
  - lib/otto/security/rate_limiter.rb
@@ -1,28 +0,0 @@
1
- Added
2
- -----
3
-
4
- - ``Otto::RequestContext`` Data class providing immutable, structured authentication context for Logic classes
5
- - Helper methods ``authenticated?``, ``has_role?``, ``has_permission?``, ``user_name``, ``session_id`` for cleaner Logic class implementation
6
- - Factory methods for creating RequestContext from AuthResult or anonymous contexts
7
-
8
- Changed
9
- -------
10
-
11
- - **BREAKING**: Logic class constructor signature changed from ``initialize(session, user, params, locale)`` to ``initialize(context, params, locale)``
12
- - Logic classes now receive immutable RequestContext instead of separate session/user parameters
13
- - LogicClassHandler simplified to single arity pattern, removing backward compatibility code
14
- - Authentication middleware now creates RequestContext instances for all requests
15
-
16
- Documentation
17
- -------------
18
-
19
- - Updated migration guide with comprehensive RequestContext examples and step-by-step conversion instructions
20
- - Updated Logic class examples in advanced_routes and authentication_strategies to demonstrate new pattern
21
- - Enhanced documentation with RequestContext API reference and helper method examples
22
-
23
- AI Assistance
24
- -------------
25
-
26
- - RequestContext Data class design developed with AI architectural guidance for immutability and clean API
27
- - Comprehensive migration of all example Logic classes with AI assistance for consistency and best practices
28
- - Documentation improvements ensuring clarity of breaking changes and migration path
@@ -1,21 +0,0 @@
1
- Changed
2
- -------
3
-
4
- - Replaced `RequestContext` with `StrategyResult` class for better authentication handling
5
- - Simplified authentication strategy API to return `StrategyResult` or `nil` for success/failure
6
- - Enhanced route handlers to support JSON request body parsing
7
- - Updated authentication middleware to use `StrategyResult` throughout
8
-
9
- Added
10
- -----
11
-
12
- - Added `StrategyResult` class with improved user model compatibility and cleaner API
13
- - Added JSON request body parsing support in Logic class handlers
14
-
15
- Removed
16
- -------
17
-
18
- - Removed `RequestContext` class (replaced by `StrategyResult`)
19
- - Removed `AuthResult` class from authentication system
20
- - Removed OpenStruct dependency across the framework
21
- - Removed `ConcurrentCacheStore` example class for an ActiveSupport::Cache::MemoryStore-compatible interface with Rack::Attack
@@ -1,21 +0,0 @@
1
- Changed
2
- -------
3
-
4
- - Reorganized Otto security module structure for better maintainability and separation of concerns
5
- - Moved authentication strategies to ``Otto::Security::Authentication::Strategies`` namespace
6
- - Moved security middleware to ``Otto::Security::Middleware`` namespace
7
- - Moved ``StrategyResult`` and ``FailureResult`` to ``Otto::Security::Authentication`` namespace
8
-
9
- Added
10
- -----
11
-
12
- - Added new modular directory structure under ``lib/otto/security/``
13
- - Added backward compatibility aliases to maintain existing API compatibility
14
- - Added proper namespacing for authentication components and middleware classes
15
-
16
- AI Assistance
17
- -------------
18
-
19
- - Comprehensive security module reorganization with systematic namespace restructuring
20
- - Automated test validation to ensure backward compatibility during refactoring
21
- - Intelligent file organization following Ruby conventions and single responsibility principles