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/Gemfile.lock CHANGED
@@ -1,8 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (2.0.0.pre.pre1)
4
+ otto (2.0.0.pre3)
5
+ concurrent-ruby (~> 1.3, < 2.0)
5
6
  facets (~> 3.1)
7
+ ipaddr (~> 1, < 2.0)
8
+ logger (~> 1, < 2.0)
6
9
  loofah (~> 2.20)
7
10
  rack (~> 3.1, < 4.0)
8
11
  rack-parser (~> 0.7)
@@ -12,6 +15,7 @@ GEM
12
15
  remote: https://rubygems.org/
13
16
  specs:
14
17
  ast (2.4.3)
18
+ benchmark (0.4.1)
15
19
  bigdecimal (3.2.3)
16
20
  concurrent-ruby (1.3.5)
17
21
  crass (1.0.6)
@@ -20,15 +24,16 @@ GEM
20
24
  irb (~> 1.10)
21
25
  reline (>= 0.3.8)
22
26
  diff-lcs (1.6.2)
23
- erb (5.0.2)
27
+ erb (5.1.1)
24
28
  facets (3.1.0)
25
29
  hana (1.3.7)
26
30
  io-console (0.8.1)
31
+ ipaddr (1.2.7)
27
32
  irb (1.15.2)
28
33
  pp (>= 0.6.0)
29
34
  rdoc (>= 4.0.0)
30
35
  reline (>= 0.4.2)
31
- json (2.13.2)
36
+ json (2.15.1)
32
37
  json_schemer (2.4.0)
33
38
  bigdecimal
34
39
  hana (~> 1.3)
@@ -40,22 +45,22 @@ GEM
40
45
  loofah (2.24.1)
41
46
  crass (~> 1.0.2)
42
47
  nokogiri (>= 1.12.0)
43
- minitest (5.25.5)
44
- nokogiri (1.18.9-aarch64-linux-gnu)
48
+ minitest (5.26.0)
49
+ nokogiri (1.18.10-aarch64-linux-gnu)
45
50
  racc (~> 1.4)
46
- nokogiri (1.18.9-aarch64-linux-musl)
51
+ nokogiri (1.18.10-aarch64-linux-musl)
47
52
  racc (~> 1.4)
48
- nokogiri (1.18.9-arm-linux-gnu)
53
+ nokogiri (1.18.10-arm-linux-gnu)
49
54
  racc (~> 1.4)
50
- nokogiri (1.18.9-arm-linux-musl)
55
+ nokogiri (1.18.10-arm-linux-musl)
51
56
  racc (~> 1.4)
52
- nokogiri (1.18.9-arm64-darwin)
57
+ nokogiri (1.18.10-arm64-darwin)
53
58
  racc (~> 1.4)
54
- nokogiri (1.18.9-x86_64-darwin)
59
+ nokogiri (1.18.10-x86_64-darwin)
55
60
  racc (~> 1.4)
56
- nokogiri (1.18.9-x86_64-linux-gnu)
61
+ nokogiri (1.18.10-x86_64-linux-gnu)
57
62
  racc (~> 1.4)
58
- nokogiri (1.18.9-x86_64-linux-musl)
63
+ nokogiri (1.18.10-x86_64-linux-musl)
59
64
  racc (~> 1.4)
60
65
  parallel (1.27.0)
61
66
  parser (3.3.9.0)
@@ -63,16 +68,16 @@ GEM
63
68
  racc
64
69
  pastel (0.8.0)
65
70
  tty-color (~> 0.5)
66
- pp (0.6.2)
71
+ pp (0.6.3)
67
72
  prettyprint
68
73
  prettier_print (1.2.1)
69
74
  prettyprint (0.2.0)
70
- prism (1.4.0)
75
+ prism (1.5.2)
71
76
  psych (5.2.6)
72
77
  date
73
78
  stringio
74
79
  racc (1.8.1)
75
- rack (3.2.1)
80
+ rack (3.2.3)
76
81
  rack-attack (6.7.0)
77
82
  rack (>= 1.0, < 4)
78
83
  rack-parser (0.7.0)
@@ -84,13 +89,14 @@ GEM
84
89
  rainbow (3.1.1)
85
90
  rbs (3.9.5)
86
91
  logger
87
- rdoc (6.14.2)
92
+ rdoc (6.15.0)
88
93
  erb
89
94
  psych (>= 4.0.0)
90
- regexp_parser (2.11.2)
95
+ tsort
96
+ regexp_parser (2.11.3)
91
97
  reline (0.6.2)
92
98
  io-console (~> 0.5)
93
- rexml (3.4.3)
99
+ rexml (3.4.4)
94
100
  rspec (3.13.1)
95
101
  rspec-core (~> 3.13.0)
96
102
  rspec-expectations (~> 3.13.0)
@@ -103,8 +109,8 @@ GEM
103
109
  rspec-mocks (3.13.5)
104
110
  diff-lcs (>= 1.2.0, < 2.0)
105
111
  rspec-support (~> 3.13.0)
106
- rspec-support (3.13.5)
107
- rubocop (1.80.2)
112
+ rspec-support (3.13.6)
113
+ rubocop (1.81.1)
108
114
  json (~> 2.3)
109
115
  language_server-protocol (~> 3.17.0.2)
110
116
  lint_roller (~> 1.1.0)
@@ -112,10 +118,10 @@ GEM
112
118
  parser (>= 3.3.0.2)
113
119
  rainbow (>= 2.2.2, < 4.0)
114
120
  regexp_parser (>= 2.9.3, < 3.0)
115
- rubocop-ast (>= 1.46.0, < 2.0)
121
+ rubocop-ast (>= 1.47.1, < 2.0)
116
122
  ruby-progressbar (~> 1.7)
117
123
  unicode-display_width (>= 2.4.0, < 4.0)
118
- rubocop-ast (1.46.0)
124
+ rubocop-ast (1.47.1)
119
125
  parser (>= 3.3.7.2)
120
126
  prism (~> 1.4)
121
127
  rubocop-performance (1.26.0)
@@ -139,15 +145,16 @@ GEM
139
145
  stringio (3.1.7)
140
146
  syntax_tree (6.3.0)
141
147
  prettier_print (>= 1.2.0)
142
- tryouts (3.6.0)
148
+ tryouts (3.6.1)
143
149
  concurrent-ruby (~> 1.0)
144
150
  irb
145
151
  minitest (~> 5.0)
146
152
  pastel (~> 0.8)
147
153
  prism (~> 1.0)
148
- rspec (~> 3.0)
154
+ rspec (>= 3.0, < 5.0)
149
155
  tty-cursor (~> 0.7)
150
156
  tty-screen (~> 0.8)
157
+ tsort (0.2.0)
151
158
  tty-color (0.6.0)
152
159
  tty-cursor (0.7.1)
153
160
  tty-screen (0.8.2)
@@ -166,6 +173,7 @@ PLATFORMS
166
173
  x86_64-linux-musl
167
174
 
168
175
  DEPENDENCIES
176
+ benchmark
169
177
  debug
170
178
  json_schemer
171
179
  otto!
@@ -173,14 +181,14 @@ DEPENDENCIES
173
181
  rack-test
174
182
  rackup
175
183
  rspec (~> 3.13)
176
- rubocop
184
+ rubocop (~> 1.81.1)
177
185
  rubocop-performance
178
186
  rubocop-rspec
179
187
  rubocop-thread_safety
180
188
  ruby-lsp
181
189
  stackprof
182
190
  syntax_tree
183
- tryouts (~> 3.6.0)
191
+ tryouts (~> 3.6.1)
184
192
 
185
193
  BUNDLED WITH
186
194
  2.7.1
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark to measure real-world Otto performance with actual routes and middleware
5
+ # Usage: ruby benchmark_middleware_wrap.rb
6
+
7
+ require 'bundler/setup'
8
+ require 'benchmark'
9
+ require 'stringio'
10
+ require 'tempfile'
11
+
12
+ require_relative 'lib/otto'
13
+
14
+ REQUEST_COUNT = 50_000
15
+ MIDDLEWARE_COUNT = 40
16
+
17
+ # Create a temporary routes file
18
+ routes_content = <<~ROUTES
19
+ GET / TestApp.index
20
+ GET /users/:id TestApp.show
21
+ POST /users TestApp.create
22
+ GET /health TestApp.health
23
+ ROUTES
24
+
25
+ routes_file = Tempfile.new(['routes', '.txt'])
26
+ routes_file.write(routes_content)
27
+ routes_file.close
28
+
29
+ # Define test application
30
+ class TestApp
31
+ def self.index(_env, _params = {})
32
+ [200, { 'Content-Type' => 'text/html' }, ['Welcome']]
33
+ end
34
+
35
+ def self.show(env, params = {})
36
+ user_id = params[:id] || env.dig('otto.params', :id) || '123'
37
+ [200, { 'Content-Type' => 'text/html' }, ["User #{user_id}"]]
38
+ end
39
+
40
+ def self.create(_env, _params = {})
41
+ [201, { 'Content-Type' => 'application/json' }, ['{"id": 123}']]
42
+ end
43
+
44
+ def self.health(_env, _params = {})
45
+ [200, { 'Content-Type' => 'text/plain' }, ['OK']]
46
+ end
47
+ end
48
+
49
+ # Create real Rack middleware
50
+ class BenchmarkMiddleware
51
+ def initialize(app, _config = nil)
52
+ @app = app
53
+ end
54
+
55
+ def call(env)
56
+ @app.call(env)
57
+ end
58
+ end
59
+
60
+ # Create Otto instance with real configuration
61
+ otto = Otto.new(routes_file.path)
62
+
63
+ # Add real Otto security middleware
64
+ otto.enable_csrf_protection!
65
+ otto.enable_request_validation!
66
+
67
+ # Add custom middleware to reach target count
68
+ current_count = otto.middleware.size
69
+ (MIDDLEWARE_COUNT - current_count).times do
70
+ otto.use(Class.new(BenchmarkMiddleware))
71
+ end
72
+
73
+ # Suppress error logging for benchmark
74
+ Otto.logger.level = Logger::FATAL
75
+
76
+ puts "\n" + ("=" * 70)
77
+ puts "Otto Performance Benchmark"
78
+ puts ("=" * 70)
79
+ puts "Configuration:"
80
+ puts " Routes: #{otto.instance_variable_get(:@route_definitions).size}"
81
+ actual_app = otto.instance_variable_get(:@app)
82
+ puts " Middleware: #{otto.middleware.size} (#{MIDDLEWARE_COUNT} total in stack, app built: #{!actual_app.nil?})"
83
+ puts " Requests: #{REQUEST_COUNT.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
84
+ puts ("=" * 70)
85
+
86
+ # Create realistic Rack environments for different routes
87
+ def make_env(method, path)
88
+ {
89
+ 'REQUEST_METHOD' => method,
90
+ 'PATH_INFO' => path,
91
+ 'QUERY_STRING' => '',
92
+ 'SERVER_NAME' => 'example.com',
93
+ 'SERVER_PORT' => '80',
94
+ 'rack.version' => [1, 3],
95
+ 'rack.url_scheme' => 'http',
96
+ 'rack.input' => StringIO.new,
97
+ 'rack.errors' => StringIO.new,
98
+ 'rack.multithread' => false,
99
+ 'rack.multiprocess' => true,
100
+ 'rack.run_once' => false,
101
+ 'REMOTE_ADDR' => '192.168.1.100',
102
+ 'HTTP_USER_AGENT' => 'Benchmark/1.0',
103
+ 'rack.session' => {}
104
+ }
105
+ end
106
+
107
+ # Test different routes
108
+ routes = [
109
+ ['GET', '/'],
110
+ ['GET', '/users/123'],
111
+ ['POST', '/users'],
112
+ ['GET', '/health']
113
+ ]
114
+
115
+ # Warmup
116
+ puts "\nWarming up (1,000 requests)..."
117
+ 1_000.times do |i|
118
+ method, path = routes[i % routes.size]
119
+ env = make_env(method, path)
120
+ otto.call(env)
121
+ end
122
+
123
+ puts "\n" + ("=" * 70)
124
+ puts "Running benchmark..."
125
+ puts ("=" * 70)
126
+
127
+ # Benchmark
128
+ result = Benchmark.measure do
129
+ REQUEST_COUNT.times do |i|
130
+ method, path = routes[i % routes.size]
131
+ env = make_env(method, path)
132
+ otto.call(env)
133
+ end
134
+ end
135
+
136
+ total_time = result.real
137
+ per_request = (total_time / REQUEST_COUNT * 1_000_000).round(2)
138
+ requests_per_sec = (REQUEST_COUNT / total_time).round(0)
139
+
140
+ puts "\nResults:"
141
+ puts ("=" * 70)
142
+ puts " Total time: #{(total_time * 1000).round(2)}ms"
143
+ puts " Time per request: #{per_request}µs"
144
+ puts " Requests/sec: #{requests_per_sec.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
145
+ puts ("=" * 70)
146
+
147
+ # Performance analysis
148
+ puts "\nPerformance Analysis:"
149
+ if per_request < 20
150
+ puts " ✓ Excellent performance (< 20µs per request)"
151
+ elsif per_request < 50
152
+ puts " ✓ Good performance (< 50µs per request)"
153
+ elsif per_request < 100
154
+ puts " ~ Acceptable performance (< 100µs per request)"
155
+ else
156
+ puts " ⚠ May need optimization (#{per_request}µs per request)"
157
+ end
158
+
159
+ puts "\nMiddleware overhead: ~#{((per_request - 2.5) / MIDDLEWARE_COUNT).round(3)}µs per middleware"
160
+ puts
161
+
162
+ # Cleanup
163
+ routes_file.unlink
@@ -0,0 +1,36 @@
1
+ Changed
2
+ -------
3
+
4
+ - Authentication now handled by RouteAuthWrapper at handler level instead of middleware
5
+ - RouteAuthWrapper enhanced with session persistence, security headers, strategy caching, and sophisticated pattern matching
6
+ - env['otto.strategy_result'] now GUARANTEED to be present on all routes (authenticated or anonymous)
7
+ - RouteAuthWrapper now wraps all route handlers, not just routes with auth requirements
8
+
9
+ Removed
10
+ -------
11
+
12
+ - Removed AuthenticationMiddleware (architecturally broken - executed before routing)
13
+ - Removed enable_authentication! (no longer needed - RouteAuthWrapper handles auth automatically)
14
+ - Removed defensive nil fallback from LogicClassHandler (no longer needed)
15
+
16
+ Fixed
17
+ -----
18
+
19
+ - Session persistence now works correctly (env['rack.session'] references same object as strategy_result.session)
20
+ - Security headers now included on all authentication failure responses (401/302)
21
+ - Strategy lookups now cached for performance
22
+ - env['otto.strategy_result'] is now guaranteed to be present (anonymous StrategyResult for public routes)
23
+ - Routes without auth requirements now get anonymous StrategyResult with IP metadata
24
+
25
+ Security
26
+ --------
27
+
28
+ - Authentication strategies now execute after routing when route_definition is available
29
+ - Supports exact match, prefix match (role:admin), and fallback patterns for strategies
30
+
31
+ Documentation
32
+ -------------
33
+
34
+ - Updated CLAUDE.md with RouteAuthWrapper architecture overview
35
+ - Updated env_keys.rb to document guaranteed presence of strategy_result
36
+ - Added comprehensive tests for anonymous route handling
@@ -0,0 +1,5 @@
1
+ Changed
2
+ -------
3
+
4
+ - Renamed MiddlewareStack#build_app to #wrap to better reflect per-request behavior
5
+ (wraps base app in middleware layers on each request, not a one-time initialization)
data/docs/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  *
2
2
  !.gitignore
3
3
  !migrating/
4
+ !migrating/*.md
5
+ !ipaddr-encoding-quirk.md
@@ -0,0 +1,34 @@
1
+ # IPAddr#to_s Encoding Quirk in Ruby 3
2
+
3
+ Ruby's `IPAddr#to_s` returns inconsistent encodings: IPv4 addresses use US-ASCII, IPv6 addresses use UTF-8.
4
+
5
+ ## Behavior
6
+
7
+ ```ruby
8
+ IPAddr.new('192.168.1.1').to_s.encoding # => #<Encoding:US-ASCII>
9
+ IPAddr.new('::1').to_s.encoding # => #<Encoding:UTF-8>
10
+ ```
11
+
12
+ ## Cause
13
+
14
+ Different string construction in IPAddr's `_to_string` method:
15
+
16
+ - **IPv4**: `Array#join('.')` → US-ASCII optimization
17
+ - **IPv6**: `String#%` → UTF-8 default
18
+
19
+ ## Impact
20
+
21
+ - Rack expects UTF-8 strings
22
+ - Mixed encodings cause `Encoding::CompatibilityError`
23
+ - String operations fail on encoding mismatches
24
+
25
+ ## Solution
26
+
27
+ Use `force_encoding('UTF-8')` instead of `encode('UTF-8')`:
28
+
29
+ - IP addresses contain only ASCII characters
30
+ - ASCII bytes are identical in US-ASCII and UTF-8
31
+ - `force_encoding` changes label only (O(1))
32
+ - `encode` creates new string (O(n))
33
+
34
+ This ensures consistent UTF-8 encoding across all IP strings.