otto 1.5.0 → 2.0.0.pre1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -5
  3. data/.github/workflows/claude-code-review.yml +53 -0
  4. data/.github/workflows/claude.yml +49 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +24 -345
  7. data/CHANGELOG.rst +83 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +21 -5
  10. data/Gemfile.lock +69 -31
  11. data/README.md +2 -0
  12. data/bin/rspec +16 -0
  13. data/changelog.d/20250911_235619_delano_next.rst +28 -0
  14. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
  15. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
  16. data/changelog.d/README.md +120 -0
  17. data/changelog.d/scriv.ini +5 -0
  18. data/docs/.gitignore +1 -0
  19. data/docs/migrating/v2.0.0-pre1.md +276 -0
  20. data/examples/.gitignore +1 -0
  21. data/examples/advanced_routes/README.md +33 -0
  22. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  27. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  29. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  30. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  31. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  32. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  33. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  34. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  35. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  36. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  37. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  38. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  39. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  40. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  41. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  42. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  43. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  45. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  46. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  47. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  48. data/examples/advanced_routes/app.rb +33 -0
  49. data/examples/advanced_routes/config.rb +23 -0
  50. data/examples/advanced_routes/config.ru +7 -0
  51. data/examples/advanced_routes/puma.rb +20 -0
  52. data/examples/advanced_routes/routes +167 -0
  53. data/examples/advanced_routes/run.rb +39 -0
  54. data/examples/advanced_routes/test.rb +58 -0
  55. data/examples/authentication_strategies/README.md +32 -0
  56. data/examples/authentication_strategies/app/auth.rb +68 -0
  57. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  58. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  59. data/examples/authentication_strategies/config.ru +24 -0
  60. data/examples/authentication_strategies/routes +37 -0
  61. data/examples/basic/README.md +29 -0
  62. data/examples/basic/app.rb +7 -35
  63. data/examples/basic/routes +0 -9
  64. data/examples/mcp_demo/README.md +87 -0
  65. data/examples/mcp_demo/app.rb +51 -0
  66. data/examples/mcp_demo/config.ru +17 -0
  67. data/examples/mcp_demo/routes +9 -0
  68. data/examples/security_features/README.md +46 -0
  69. data/examples/security_features/app.rb +23 -24
  70. data/examples/security_features/config.ru +8 -10
  71. data/lib/otto/core/configuration.rb +167 -0
  72. data/lib/otto/core/error_handler.rb +86 -0
  73. data/lib/otto/core/file_safety.rb +61 -0
  74. data/lib/otto/core/middleware_stack.rb +157 -0
  75. data/lib/otto/core/router.rb +183 -0
  76. data/lib/otto/core/uri_generator.rb +44 -0
  77. data/lib/otto/design_system.rb +7 -5
  78. data/lib/otto/helpers/base.rb +3 -0
  79. data/lib/otto/helpers/request.rb +10 -8
  80. data/lib/otto/helpers/response.rb +5 -4
  81. data/lib/otto/helpers/validation.rb +85 -0
  82. data/lib/otto/mcp/auth/token.rb +77 -0
  83. data/lib/otto/mcp/protocol.rb +164 -0
  84. data/lib/otto/mcp/rate_limiting.rb +155 -0
  85. data/lib/otto/mcp/registry.rb +100 -0
  86. data/lib/otto/mcp/route_parser.rb +77 -0
  87. data/lib/otto/mcp/server.rb +206 -0
  88. data/lib/otto/mcp/validation.rb +123 -0
  89. data/lib/otto/response_handlers/auto.rb +39 -0
  90. data/lib/otto/response_handlers/base.rb +16 -0
  91. data/lib/otto/response_handlers/default.rb +16 -0
  92. data/lib/otto/response_handlers/factory.rb +39 -0
  93. data/lib/otto/response_handlers/json.rb +28 -0
  94. data/lib/otto/response_handlers/redirect.rb +25 -0
  95. data/lib/otto/response_handlers/view.rb +24 -0
  96. data/lib/otto/response_handlers.rb +9 -135
  97. data/lib/otto/route.rb +9 -9
  98. data/lib/otto/route_definition.rb +30 -33
  99. data/lib/otto/route_handlers/base.rb +121 -0
  100. data/lib/otto/route_handlers/class_method.rb +89 -0
  101. data/lib/otto/route_handlers/factory.rb +29 -0
  102. data/lib/otto/route_handlers/instance_method.rb +69 -0
  103. data/lib/otto/route_handlers/lambda.rb +59 -0
  104. data/lib/otto/route_handlers/logic_class.rb +93 -0
  105. data/lib/otto/route_handlers.rb +10 -376
  106. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  107. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  108. data/lib/otto/security/authentication/failure_result.rb +36 -0
  109. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  111. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  112. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  113. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  114. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  115. data/lib/otto/security/authentication.rb +28 -282
  116. data/lib/otto/security/config.rb +15 -11
  117. data/lib/otto/security/configurator.rb +219 -0
  118. data/lib/otto/security/csrf.rb +8 -143
  119. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  120. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  121. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  122. data/lib/otto/security/rate_limiter.rb +86 -0
  123. data/lib/otto/security/rate_limiting.rb +16 -0
  124. data/lib/otto/security/validator.rb +8 -292
  125. data/lib/otto/static.rb +3 -0
  126. data/lib/otto/utils.rb +14 -0
  127. data/lib/otto/version.rb +3 -1
  128. data/lib/otto.rb +184 -414
  129. data/otto.gemspec +11 -6
  130. metadata +134 -25
  131. data/examples/dynamic_pages/app.rb +0 -115
  132. data/examples/dynamic_pages/config.ru +0 -30
  133. data/examples/dynamic_pages/routes +0 -21
  134. data/examples/helpers_demo/app.rb +0 -244
  135. data/examples/helpers_demo/config.ru +0 -26
  136. data/examples/helpers_demo/routes +0 -7
@@ -1,244 +0,0 @@
1
- require 'otto'
2
- require 'json'
3
-
4
- class HelpersDemo
5
- def initialize(req, res)
6
- @req, @res = req, res
7
- end
8
-
9
- attr_reader :req, :res
10
-
11
- def index
12
- res.headers['content-type'] = 'text/html'
13
- res.body = <<~HTML
14
- <h1>Otto Request & Response Helpers Demo</h1>
15
- <p>This demo shows Otto's built-in request and response helpers.</p>
16
-
17
- <h2>Available Demos:</h2>
18
- <ul>
19
- <li><a href="/request-info">Request Information</a> - Shows client IP, user agent, security info</li>
20
- <li><a href="/locale-demo?locale=es">Locale Detection</a> - Demonstrates locale detection and configuration</li>
21
- <li><a href="/secure-cookie">Secure Cookies</a> - Sets secure cookies with proper options</li>
22
- <li><a href="/headers">Response Headers</a> - Shows security headers and custom headers</li>
23
- <li>
24
- <form method="POST" action="/csp-demo">
25
- <button type="submit">CSP Headers Demo</button> - Content Security Policy with nonce
26
- </form>
27
- </li>
28
- </ul>
29
-
30
- <h2>Try These URLs:</h2>
31
- <ul>
32
- <li><a href="/locale-demo?locale=fr">French locale</a></li>
33
- <li><a href="/locale-demo?locale=invalid">Invalid locale (falls back to default)</a></li>
34
- </ul>
35
- HTML
36
- end
37
-
38
- def request_info
39
- # Demonstrate request helpers
40
- info = {
41
- 'Client IP' => req.client_ipaddress,
42
- 'User Agent' => req.user_agent,
43
- 'HTTP Host' => req.http_host,
44
- 'Server Name' => req.current_server_name,
45
- 'Request Path' => req.request_path,
46
- 'Request URI' => req.request_uri,
47
- 'Is Local?' => req.local?,
48
- 'Is Secure?' => req.secure?,
49
- 'Is AJAX?' => req.ajax?,
50
- 'Current Absolute URI' => req.current_absolute_uri,
51
- 'Request Method' => req.request_method
52
- }
53
-
54
- # Show collected proxy headers
55
- proxy_headers = req.collect_proxy_headers(
56
- header_prefix: 'X_DEMO_',
57
- additional_keys: ['HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE']
58
- )
59
-
60
- # Format request details for logging
61
- request_details = req.format_request_details(header_prefix: 'X_DEMO_')
62
-
63
- res.headers['content-type'] = 'text/html'
64
- res.body = <<~HTML
65
- <h1>Request Information</h1>
66
- <p><a href="/">← Back to index</a></p>
67
-
68
- <h2>Basic Request Info:</h2>
69
- <table border="1" style="border-collapse: collapse;">
70
- #{info.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
71
- </table>
72
-
73
- <h2>Proxy Headers:</h2>
74
- <pre>#{proxy_headers}</pre>
75
-
76
- <h2>Formatted Request Details (for logging):</h2>
77
- <pre>#{request_details}</pre>
78
-
79
- <h2>Application Path Helper:</h2>
80
- <p>App path for ['api', 'v1', 'users']: <code>#{req.app_path('api', 'v1', 'users')}</code></p>
81
- HTML
82
- end
83
-
84
- def locale_demo
85
- # Demonstrate locale detection with Otto configuration
86
- current_locale = req.check_locale!(req.params['locale'], {
87
- preferred_locale: 'es', # Simulate user preference
88
- locale_env_key: 'demo.locale',
89
- debug: true
90
- })
91
-
92
- # Show what was stored in environment
93
- stored_locale = req.env['demo.locale']
94
-
95
- res.headers['content-type'] = 'text/html'
96
- res.body = <<~HTML
97
- <h1>Locale Detection Demo</h1>
98
- <p><a href="/">← Back to index</a></p>
99
-
100
- <h2>Locale Detection Results:</h2>
101
- <table border="1" style="border-collapse: collapse;">
102
- <tr><td><strong>Detected Locale</strong></td><td>#{current_locale}</td></tr>
103
- <tr><td><strong>Stored in Environment</strong></td><td>#{stored_locale}</td></tr>
104
- <tr><td><strong>Query Parameter</strong></td><td>#{req.params['locale'] || 'none'}</td></tr>
105
- <tr><td><strong>Accept-Language Header</strong></td><td>#{req.env['HTTP_ACCEPT_LANGUAGE'] || 'none'}</td></tr>
106
- </table>
107
-
108
- <h2>Locale Sources (in precedence order):</h2>
109
- <ol>
110
- <li>URL Parameter: <code>?locale=#{req.params['locale'] || 'none'}</code></li>
111
- <li>User Preference: <code>es</code> (simulated)</li>
112
- <li>Rack Locale: <code>#{req.env['rack.locale']&.first || 'none'}</code></li>
113
- <li>Default: <code>en</code></li>
114
- </ol>
115
-
116
- <h2>Try Different Locales:</h2>
117
- <ul>
118
- <li><a href="/locale-demo?locale=en">English (en)</a></li>
119
- <li><a href="/locale-demo?locale=es">Spanish (es)</a></li>
120
- <li><a href="/locale-demo?locale=fr">French (fr)</a></li>
121
- <li><a href="/locale-demo?locale=invalid">Invalid locale</a></li>
122
- <li><a href="/locale-demo">No locale parameter</a></li>
123
- </ul>
124
- HTML
125
- end
126
-
127
- def secure_cookie
128
- # Demonstrate secure cookie helpers
129
- res.send_secure_cookie('demo_secure', 'secure_value_123', 3600, {
130
- path: '/helpers_demo',
131
- secure: !req.local?, # Only secure in production
132
- same_site: :strict
133
- })
134
-
135
- res.send_session_cookie('demo_session', 'session_value_456', {
136
- path: '/helpers_demo'
137
- })
138
-
139
- res.headers['content-type'] = 'text/html'
140
- res.body = <<~HTML
141
- <h1>Secure Cookies Demo</h1>
142
- <p><a href="/">← Back to index</a></p>
143
-
144
- <h2>Cookies Set:</h2>
145
- <ul>
146
- <li><strong>demo_secure</strong> - Secure cookie with 1 hour TTL</li>
147
- <li><strong>demo_session</strong> - Session cookie (no expiration)</li>
148
- </ul>
149
-
150
- <h2>Cookie Security Features:</h2>
151
- <ul>
152
- <li>Secure flag (HTTPS only in production)</li>
153
- <li>HttpOnly flag (prevents XSS access)</li>
154
- <li>SameSite=Strict (CSRF protection)</li>
155
- <li>Proper expiration handling</li>
156
- </ul>
157
-
158
- <p>Check your browser's developer tools to see the cookie headers!</p>
159
- HTML
160
- end
161
-
162
- def csp_demo
163
- # Demonstrate CSP headers with nonce
164
- nonce = SecureRandom.base64(16)
165
-
166
- res.send_csp_headers('text/html; charset=utf-8', nonce, {
167
- development_mode: req.local?,
168
- debug: true
169
- })
170
-
171
- res.body = <<~HTML
172
- <h1>Content Security Policy Demo</h1>
173
- <p><a href="/">← Back to index</a></p>
174
-
175
- <h2>CSP Header Generated</h2>
176
- <p>This page includes a CSP header with a nonce. Check the response headers!</p>
177
-
178
- <h2>Nonce Value:</h2>
179
- <p><code>#{nonce}</code></p>
180
-
181
- <h2>Inline Script with Nonce:</h2>
182
- <script nonce="#{nonce}">
183
- console.log('This script runs because it has the correct nonce!');
184
- document.addEventListener('DOMContentLoaded', function() {
185
- document.getElementById('nonce-demo').innerHTML = 'Nonce verification successful!';
186
- });
187
- </script>
188
-
189
- <div id="nonce-demo" style="padding: 10px; background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
190
- Loading...
191
- </div>
192
-
193
- <p><strong>Note:</strong> Without the nonce, inline scripts would be blocked by CSP.</p>
194
- HTML
195
- end
196
-
197
- def show_headers
198
- # Demonstrate response headers and security features
199
- res.set_cookie('demo_header', {
200
- value: 'header_demo_value',
201
- max_age: 1800,
202
- secure: !req.local?,
203
- httponly: true
204
- })
205
-
206
- # Add cache control
207
- res.no_cache!
208
-
209
- # Get security headers that would be added
210
- security_headers = res.cookie_security_headers
211
-
212
- res.headers['content-type'] = 'text/html'
213
- res.headers['X-Demo-Header'] = 'Custom header value'
214
-
215
- res.body = <<~HTML
216
- <h1>Response Headers Demo</h1>
217
- <p><a href="/">← Back to index</a></p>
218
-
219
- <h2>Custom Headers Set:</h2>
220
- <ul>
221
- <li><strong>X-Demo-Header:</strong> Custom header value</li>
222
- <li><strong>Cache-Control:</strong> no-store, no-cache, must-revalidate, max-age=0</li>
223
- <li><strong>Set-Cookie:</strong> demo_header (with security options)</li>
224
- </ul>
225
-
226
- <h2>Security Headers Available:</h2>
227
- <table border="1" style="border-collapse: collapse;">
228
- #{security_headers.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
229
- </table>
230
-
231
- <p>Use your browser's developer tools to inspect all response headers!</p>
232
- HTML
233
- end
234
-
235
- def not_found
236
- res.status = 404
237
- res.headers['content-type'] = 'text/html'
238
- res.body = <<~HTML
239
- <h1>404 - Page Not Found</h1>
240
- <p><a href="/">← Back to index</a></p>
241
- <p>This is a custom 404 page demonstrating error handling.</p>
242
- HTML
243
- end
244
- end
@@ -1,26 +0,0 @@
1
- require_relative '../../lib/otto'
2
- require_relative 'app'
3
-
4
- # Global configuration for all Otto instances
5
- Otto.configure do |opts|
6
- opts.available_locales = {
7
- 'en' => 'English',
8
- 'es' => 'Spanish',
9
- 'fr' => 'French'
10
- }
11
- opts.default_locale = 'en'
12
- end
13
-
14
- # Configure Otto with security features
15
- app = Otto.new("./routes", {
16
- # Security features
17
- csrf_protection: true,
18
- request_validation: true,
19
- trusted_proxies: ['127.0.0.1', '::1']
20
- })
21
-
22
- # Enable additional security headers
23
- app.enable_csp_with_nonce!(debug: true)
24
- app.enable_frame_protection!('SAMEORIGIN')
25
-
26
- run app
@@ -1,7 +0,0 @@
1
- GET / HelpersDemo#index
2
- GET /request-info HelpersDemo#request_info
3
- GET /locale-demo HelpersDemo#locale_demo
4
- GET /secure-cookie HelpersDemo#secure_cookie
5
- POST /csp-demo HelpersDemo#csp_demo
6
- GET /headers HelpersDemo#show_headers
7
- GET /404 HelpersDemo#not_found