otto 1.6.0 → 2.0.0.pre2

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 +3 -2
  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 +26 -344
  7. data/CHANGELOG.rst +131 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +11 -4
  10. data/Gemfile.lock +38 -42
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  13. data/changelog.d/README.md +120 -0
  14. data/changelog.d/scriv.ini +5 -0
  15. data/docs/.gitignore +2 -0
  16. data/docs/migrating/v2.0.0-pre1.md +276 -0
  17. data/docs/migrating/v2.0.0-pre2.md +345 -0
  18. data/examples/.gitignore +1 -0
  19. data/examples/advanced_routes/README.md +33 -0
  20. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  21. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  22. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  27. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  29. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  30. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  31. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  32. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  33. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  34. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  35. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  36. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  37. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  38. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  39. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  40. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  41. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  42. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  43. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  45. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  46. data/examples/advanced_routes/app.rb +33 -0
  47. data/examples/advanced_routes/config.rb +23 -0
  48. data/examples/advanced_routes/config.ru +7 -0
  49. data/examples/advanced_routes/puma.rb +20 -0
  50. data/examples/advanced_routes/routes +167 -0
  51. data/examples/advanced_routes/run.rb +39 -0
  52. data/examples/advanced_routes/test.rb +58 -0
  53. data/examples/authentication_strategies/README.md +32 -0
  54. data/examples/authentication_strategies/app/auth.rb +68 -0
  55. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  56. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  57. data/examples/authentication_strategies/config.ru +24 -0
  58. data/examples/authentication_strategies/routes +37 -0
  59. data/examples/basic/README.md +29 -0
  60. data/examples/basic/app.rb +7 -35
  61. data/examples/basic/routes +0 -9
  62. data/examples/mcp_demo/README.md +87 -0
  63. data/examples/mcp_demo/app.rb +29 -34
  64. data/examples/mcp_demo/config.ru +9 -60
  65. data/examples/security_features/README.md +46 -0
  66. data/examples/security_features/app.rb +23 -24
  67. data/examples/security_features/config.ru +8 -10
  68. data/lib/otto/core/configuration.rb +167 -0
  69. data/lib/otto/core/error_handler.rb +86 -0
  70. data/lib/otto/core/file_safety.rb +61 -0
  71. data/lib/otto/core/middleware_stack.rb +237 -0
  72. data/lib/otto/core/router.rb +184 -0
  73. data/lib/otto/core/uri_generator.rb +44 -0
  74. data/lib/otto/design_system.rb +7 -5
  75. data/lib/otto/env_keys.rb +114 -0
  76. data/lib/otto/helpers/base.rb +5 -21
  77. data/lib/otto/helpers/request.rb +10 -8
  78. data/lib/otto/helpers/response.rb +27 -4
  79. data/lib/otto/helpers/validation.rb +9 -7
  80. data/lib/otto/mcp/auth/token.rb +10 -9
  81. data/lib/otto/mcp/protocol.rb +24 -27
  82. data/lib/otto/mcp/rate_limiting.rb +8 -3
  83. data/lib/otto/mcp/registry.rb +7 -2
  84. data/lib/otto/mcp/route_parser.rb +10 -15
  85. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
  86. data/lib/otto/mcp/server.rb +45 -22
  87. data/lib/otto/response_handlers/auto.rb +39 -0
  88. data/lib/otto/response_handlers/base.rb +16 -0
  89. data/lib/otto/response_handlers/default.rb +16 -0
  90. data/lib/otto/response_handlers/factory.rb +39 -0
  91. data/lib/otto/response_handlers/json.rb +34 -0
  92. data/lib/otto/response_handlers/redirect.rb +25 -0
  93. data/lib/otto/response_handlers/view.rb +24 -0
  94. data/lib/otto/response_handlers.rb +9 -135
  95. data/lib/otto/route.rb +51 -55
  96. data/lib/otto/route_definition.rb +15 -18
  97. data/lib/otto/route_handlers/base.rb +121 -0
  98. data/lib/otto/route_handlers/class_method.rb +89 -0
  99. data/lib/otto/route_handlers/factory.rb +42 -0
  100. data/lib/otto/route_handlers/instance_method.rb +69 -0
  101. data/lib/otto/route_handlers/lambda.rb +59 -0
  102. data/lib/otto/route_handlers/logic_class.rb +93 -0
  103. data/lib/otto/route_handlers.rb +10 -405
  104. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  105. data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
  106. data/lib/otto/security/authentication/failure_result.rb +44 -0
  107. data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  111. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  112. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  113. data/lib/otto/security/authentication/strategy_result.rb +337 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -23
  116. data/lib/otto/security/configurator.rb +219 -0
  117. data/lib/otto/security/csrf.rb +8 -143
  118. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  119. data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
  120. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  121. data/lib/otto/security/rate_limiter.rb +86 -0
  122. data/lib/otto/security/rate_limiting.rb +10 -105
  123. data/lib/otto/security/validator.rb +8 -253
  124. data/lib/otto/static.rb +3 -0
  125. data/lib/otto/utils.rb +14 -0
  126. data/lib/otto/version.rb +3 -1
  127. data/lib/otto.rb +141 -498
  128. data/otto.gemspec +4 -2
  129. metadata +99 -18
  130. data/examples/dynamic_pages/app.rb +0 -115
  131. data/examples/dynamic_pages/config.ru +0 -30
  132. data/examples/dynamic_pages/routes +0 -21
  133. data/examples/helpers_demo/app.rb +0 -244
  134. data/examples/helpers_demo/config.ru +0 -26
  135. data/examples/helpers_demo/routes +0 -7
  136. data/lib/concurrent_cache_store.rb +0 -68
@@ -0,0 +1,276 @@
1
+ # Migrating to Otto v2.0.0-pre1
2
+
3
+ ## What's New in v2.0.0-pre1
4
+
5
+ This pre-release includes extensive test coverage improvements (76 new test cases), core module refactoring, middleware stack unification, and a major update to Logic class authentication patterns. See the [full changelog](../../CHANGELOG.rst#changelog-2.0.0-pre1) for complete details.
6
+
7
+ The main breaking changes affect:
8
+ 1. Applications that directly manipulate the middleware stack
9
+ 2. Logic classes using the old authentication signature
10
+
11
+ ## Middleware Stack Unified API
12
+
13
+ Otto v2.0.0-pre1 introduces a significant refactoring of the middleware stack management, providing a more consistent and efficient approach to middleware configuration.
14
+
15
+ ### Key Changes
16
+
17
+ #### Unified Middleware Registration
18
+
19
+ Previous versions had separate legacy and new middleware stacks. In v2.0.0-pre1, we've consolidated these into a single, more powerful middleware stack.
20
+
21
+ **Before:**
22
+ ```ruby
23
+ # Old approach with separate stacks
24
+ otto.middleware_stack << SomeMiddleware
25
+ otto.middleware.add(AnotherMiddleware)
26
+ ```
27
+
28
+ **After:**
29
+ ```ruby
30
+ # Unified middleware registration
31
+ otto.use(SomeMiddleware)
32
+ otto.use(AnotherMiddleware)
33
+ ```
34
+
35
+ #### Performance Improvements
36
+
37
+ - Middleware lookup is now O(1) using a Set
38
+ - Memoized middleware list reduces repeated array creation
39
+ - Prevent duplicate middleware registrations with identical configurations
40
+
41
+ ### Migration Steps
42
+
43
+ 1. Replace all `otto.middleware_stack <<` calls with `otto.use()`
44
+ 2. Remove any direct references to `otto.middleware_stack`
45
+ 3. Use `otto.middleware.includes?()` instead of manual stack checks
46
+
47
+ ### Authentication and Security Methods
48
+
49
+ Authentication and security methods now consistently use the new unified middleware stack:
50
+
51
+ ```ruby
52
+ # Before
53
+ otto.middleware_stack << Otto::Security::CSRFMiddleware
54
+
55
+ # After (no change needed)
56
+ otto.enable_csrf_protection!
57
+ ```
58
+
59
+ ### Performance Considerations
60
+
61
+ The new implementation is more memory-efficient and provides faster middleware lookups, especially for applications with many middleware components.
62
+
63
+ ### Potential Breaking Changes
64
+
65
+ - Code relying on direct manipulation of `middleware_stack` will need updates
66
+ - Method signatures for middleware configuration remain the same
67
+
68
+ ### Example Migration
69
+
70
+ ```ruby
71
+ # Old code
72
+ class MyApp
73
+ def initialize
74
+ @otto = Otto.new
75
+ @otto.middleware_stack << CustomMiddleware
76
+ @otto.middleware.add(AnotherMiddleware)
77
+ end
78
+ end
79
+
80
+ # New code
81
+ class MyApp
82
+ def initialize
83
+ @otto = Otto.new
84
+ @otto.use(CustomMiddleware)
85
+ @otto.use(AnotherMiddleware)
86
+ end
87
+ end
88
+ ```
89
+
90
+ ### Troubleshooting
91
+
92
+ If you encounter any issues with middleware registration or configuration, please file an issue at [Otto GitHub Repository](https://github.com/delano/otto/issues).
93
+
94
+ ## Logic Class RequestContext Pattern
95
+
96
+ Otto v2.0.0-pre1 introduces a major improvement to Logic class authentication with the new RequestContext pattern.
97
+
98
+ ### Key Changes
99
+
100
+ #### New Constructor Signature
101
+
102
+ Logic classes now use a cleaner, more powerful constructor signature that provides immutable context.
103
+
104
+ **Before:**
105
+ ```ruby
106
+ class MyLogic
107
+ attr_reader :session, :user, :params, :locale
108
+
109
+ def initialize(session, user, params, locale)
110
+ @session = session
111
+ @user = user
112
+ @params = params
113
+ @locale = locale
114
+ end
115
+
116
+ def process
117
+ return { error: 'not authenticated' } unless @user
118
+ { result: 'success', user_name: @user['name'] }
119
+ end
120
+ end
121
+ ```
122
+
123
+ **After:**
124
+ ```ruby
125
+ class MyLogic
126
+ attr_reader :context, :params, :locale
127
+
128
+ def initialize(context, params, locale)
129
+ @context = context
130
+ @params = params
131
+ @locale = locale
132
+ end
133
+
134
+ def process
135
+ return { error: 'not authenticated' } unless @context.authenticated?
136
+ {
137
+ result: 'success',
138
+ user_name: @context.user_name,
139
+ roles: @context.roles,
140
+ permissions: @context.permissions
141
+ }
142
+ end
143
+ end
144
+ ```
145
+
146
+ #### RequestContext Benefits
147
+
148
+ 1. **Immutable Structure** - RequestContext is a Ruby Data class that can't be accidentally modified
149
+ 2. **Helper Methods** - Built-in methods like `authenticated?`, `has_role?`, `has_permission?`
150
+ 3. **Cleaner Interface** - Single context parameter instead of separate session/user parameters
151
+ 4. **Type Safety** - Better IDE support and documentation
152
+ 5. **Future-proof** - New authentication data automatically available
153
+
154
+ #### RequestContext API
155
+
156
+ ```ruby
157
+ # Authentication status
158
+ context.authenticated? # Boolean: has non-empty user data
159
+ context.anonymous? # Boolean: not authenticated
160
+
161
+ # User information
162
+ context.user_id # User ID from various possible locations
163
+ context.user_name # User name/username
164
+ context.session_id # Session identifier
165
+
166
+ # Role and permission checks
167
+ context.has_role?('admin') # Single role check
168
+ context.has_permission?('write') # Single permission check
169
+ context.has_any_role?('admin', 'mod') # Multiple role check
170
+ context.roles # Array of all user roles
171
+ context.permissions # Array of all user permissions
172
+
173
+ # Raw data access
174
+ context.session # Session hash
175
+ context.user # User hash
176
+ context.auth_method # Authentication method used
177
+ context.metadata # Additional context data
178
+ ```
179
+
180
+ ### Migration Steps
181
+
182
+ 1. **Update Logic class constructor signatures** from 4 parameters to 3:
183
+ ```ruby
184
+ # Change this:
185
+ def initialize(session, user, params, locale)
186
+
187
+ # To this:
188
+ def initialize(context, params, locale)
189
+ ```
190
+
191
+ 2. **Update instance variables**:
192
+ ```ruby
193
+ # Change this:
194
+ @session = session
195
+ @user = user
196
+
197
+ # To this:
198
+ @context = context
199
+ ```
200
+
201
+ 3. **Update authentication checks**:
202
+ ```ruby
203
+ # Change this:
204
+ return error unless @user
205
+
206
+ # To this:
207
+ return error unless @context.authenticated?
208
+ ```
209
+
210
+ 4. **Update data access**:
211
+ ```ruby
212
+ # Change this:
213
+ user_name = @user&.dig('name')
214
+ user_role = @user&.dig('role')
215
+
216
+ # To this:
217
+ user_name = @context.user_name
218
+ user_role = @context.roles.first
219
+ ```
220
+
221
+ ### Example Migration
222
+
223
+ **Before (v1.x):**
224
+ ```ruby
225
+ class AdminPanel
226
+ attr_reader :session, :user, :params, :locale
227
+
228
+ def initialize(session, user, params, locale)
229
+ @session = session
230
+ @user = user
231
+ @params = params
232
+ @locale = locale
233
+ end
234
+
235
+ def raise_concerns
236
+ raise 'Access denied' unless @user&.dig('role') == 'admin'
237
+ end
238
+
239
+ def process
240
+ {
241
+ panel: 'admin',
242
+ user: @user&.dig('name') || 'unknown',
243
+ session_id: @session&.dig('id')
244
+ }
245
+ end
246
+ end
247
+ ```
248
+
249
+ **After (v2.0):**
250
+ ```ruby
251
+ class AdminPanel
252
+ attr_reader :context, :params, :locale
253
+
254
+ def initialize(context, params, locale)
255
+ @context = context
256
+ @params = params
257
+ @locale = locale
258
+ end
259
+
260
+ def raise_concerns
261
+ raise 'Access denied' unless @context.has_role?('admin')
262
+ end
263
+
264
+ def process
265
+ {
266
+ panel: 'admin',
267
+ user: @context.user_name || 'unknown',
268
+ session_id: @context.session_id,
269
+ authenticated: @context.authenticated?,
270
+ permissions: @context.permissions
271
+ }
272
+ end
273
+ end
274
+ ```
275
+
276
+ This provides a cleaner, more maintainable interface while giving Logic classes access to rich authentication context.
@@ -0,0 +1,345 @@
1
+ # Otto v2.0.0-pre2 Migration Guide
2
+
3
+ ## Overview
4
+
5
+ This release resolves critical architectural issues with `StrategyResult` semantics and removes deprecated methods. The changes clarify the distinction between "request state" (user in session) and "authentication outcomes" (auth attempt just succeeded).
6
+
7
+ ## Breaking Changes
8
+
9
+ ### 1. StrategyResult Methods Removed
10
+
11
+ **Removed Methods:**
12
+ - `StrategyResult#success?` - Always returned `true`, meaningless
13
+ - `StrategyResult#failure?` - Always returned `false`, meaningless
14
+ - `FailureResult#success?` - Always returned `false`
15
+ - `FailureResult#failure?` - Always returned `true`
16
+
17
+ **Migration:**
18
+
19
+ ```ruby
20
+ # Before - checking success/failure
21
+ if strategy_result&.success?
22
+ # handle success
23
+ end
24
+
25
+ # After - type checking
26
+ if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult)
27
+ # handle success
28
+ end
29
+
30
+ # Or for middleware - FailureResult indicates failure
31
+ if strategy_result.is_a?(Otto::Security::Authentication::FailureResult)
32
+ # handle failure
33
+ else
34
+ # handle success
35
+ end
36
+ ```
37
+
38
+ ### 2. New Semantic Distinction
39
+
40
+ **New Method:**
41
+ - `StrategyResult#auth_attempt_succeeded?` - Returns `true` only when auth strategy just executed successfully
42
+
43
+ **Key Semantic Difference:**
44
+
45
+ | Method | Meaning | Use Case |
46
+ |--------|---------|----------|
47
+ | `authenticated?` | User in session (request state) | Check if session has user |
48
+ | `auth_attempt_succeeded?` | Auth strategy just succeeded (auth outcome) | Post-login redirects, analytics |
49
+
50
+ **Migration Examples:**
51
+
52
+ #### Registration Flow (IMPORTANT)
53
+
54
+ ```ruby
55
+ # Before - BROKEN - blocks legitimate registration
56
+ class CreateAccount < Logic::Base
57
+ def raise_concerns
58
+ # This was always true if user in session, blocking registration
59
+ raise OT::FormError, "Already signed up" if @strategy_result.success?
60
+ end
61
+ end
62
+
63
+ # After - CORRECT
64
+ class CreateAccount < Logic::Base
65
+ def raise_concerns
66
+ # Check if user already in session
67
+ raise OT::FormError, "Already signed up" if @strategy_result.authenticated?
68
+ end
69
+ end
70
+ ```
71
+
72
+ #### Post-Login Redirect
73
+
74
+ ```ruby
75
+ # Before - unreliable
76
+ class AuthController
77
+ def authenticate
78
+ if @strategy_result.success? # Always true, not helpful
79
+ redirect_to dashboard_path
80
+ end
81
+ end
82
+ end
83
+
84
+ # After - correct semantic
85
+ class AuthController
86
+ def authenticate
87
+ if @strategy_result.auth_attempt_succeeded?
88
+ # Only redirect when auth route just succeeded
89
+ redirect_to dashboard_path
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ ## Non-Breaking Enhancements
96
+
97
+ ### 1. Comprehensive Documentation
98
+
99
+ `StrategyResult` now includes extensive inline documentation:
100
+ - Usage patterns and creation guidelines
101
+ - Session contract for multi-app architectures
102
+ - Examples for common scenarios
103
+ - Clear distinction between request state and auth outcomes
104
+
105
+ ### 2. Session Contract (Multi-App Architectures)
106
+
107
+ For shared session architectures (Auth app + Core app):
108
+
109
+ **Required session keys for authenticated state:**
110
+ ```ruby
111
+ session['authenticated'] # Boolean flag
112
+ session['identity_id'] # User/customer ID
113
+ session['authenticated_at'] # Timestamp
114
+ ```
115
+
116
+ **Optional session keys:**
117
+ ```ruby
118
+ session['email'] # User email
119
+ session['ip_address'] # Client IP
120
+ session['user_agent'] # Client UA
121
+ session['locale'] # User locale
122
+ ```
123
+
124
+ **Advanced mode adds:**
125
+ ```ruby
126
+ session['account_external_id'] # Rodauth external_id
127
+ session['advanced_account_id'] # Rodauth account ID
128
+ ```
129
+
130
+ ## Application Code Updates Required
131
+
132
+ ### 1. Remove Manual StrategyResult Creation
133
+
134
+ **Anti-pattern identified:**
135
+ ```ruby
136
+ # BAD - Bypasses Otto's auth_method tracking
137
+ class Controller::Base
138
+ def _strategy_result
139
+ Otto::Security::Authentication::StrategyResult.new(
140
+ session: session,
141
+ user: cust,
142
+ auth_method: 'session', # Hardcoded - loses semantic meaning
143
+ metadata: { ip: req.client_ipaddress }
144
+ )
145
+ end
146
+ end
147
+ ```
148
+
149
+ **Correct approach:**
150
+ ```ruby
151
+ # GOOD - Use middleware-provided result
152
+ class Controller::Base
153
+ def strategy_result
154
+ req.env['otto.strategy_result'] # Created by AuthenticationMiddleware
155
+ end
156
+ end
157
+
158
+ # Or for non-auth checks, use session directly
159
+ class Controller::Base
160
+ def current_user
161
+ return nil unless session['authenticated']
162
+ Customer.find(session['identity_id'])
163
+ end
164
+ end
165
+ ```
166
+
167
+ ### 2. Update Logic Classes
168
+
169
+ **Pattern to check for:**
170
+ ```ruby
171
+ # Search your codebase for these patterns:
172
+ grep -r "@strategy_result.success?" apps/
173
+ grep -r "@context.success?" apps/
174
+ grep -r "strategy_result&.success?" apps/
175
+ ```
176
+
177
+ **Update to:**
178
+ - Use `authenticated?` for "user in session" checks (registration, profile access, etc.)
179
+ - Use `auth_attempt_succeeded?` for "just logged in" checks (redirects, welcome messages, etc.)
180
+
181
+ ### 3. Test Updates
182
+
183
+ **RSpec matchers:**
184
+ ```ruby
185
+ # Before
186
+ expect(result).to be_success
187
+ expect(result).to be_failure
188
+
189
+ # After
190
+ expect(result).to be_a(Otto::Security::Authentication::StrategyResult)
191
+ expect(result).to be_a(Otto::Security::Authentication::FailureResult)
192
+ ```
193
+
194
+ ## Architecture Clarifications
195
+
196
+ ### When StrategyResult is Created
197
+
198
+ 1. **Routes WITH `auth=...` requirement:**
199
+ - Strategy executes
200
+ - Returns `StrategyResult` (success) or `FailureResult` (failure)
201
+ - Middleware converts `FailureResult` to anonymous `StrategyResult` + 401 response
202
+
203
+ 2. **Routes WITHOUT `auth=...` requirement:**
204
+ - Middleware creates anonymous `StrategyResult`
205
+ - Sets `auth_method: 'anonymous'`
206
+
207
+ 3. **Auth app (Roda) routes:**
208
+ - Manually creates `StrategyResult` for Logic class compatibility
209
+ - Same interface as Otto controllers
210
+
211
+ ### Integration Boundaries
212
+
213
+ **Multi-app setup (Auth + Core + API):**
214
+ - **Shared:** Session middleware, Redis session, Logic classes, Customer model
215
+ - **Auth app:** Creates StrategyResult manually, uses Roda routing
216
+ - **Core/API apps:** StrategyResult from AuthenticationMiddleware
217
+ - **Integration:** Pure session-based, no direct code calls between apps
218
+
219
+ ## Testing Your Migration
220
+
221
+ ### 1. Registration Flow Test
222
+
223
+ ```ruby
224
+ describe "CreateAccount" do
225
+ it "blocks registration when user already authenticated" do
226
+ strategy_result = Otto::Security::Authentication::StrategyResult.new(
227
+ session: { user_id: 123 },
228
+ user: { id: 123 },
229
+ auth_method: 'anonymous', # No auth route, but user in session
230
+ metadata: {}
231
+ )
232
+
233
+ logic = CreateAccount.new(strategy_result, params, 'en')
234
+
235
+ expect { logic.raise_concerns }.to raise_error(OT::FormError, /Already signed up/)
236
+ end
237
+ end
238
+ ```
239
+
240
+ ### 2. Auth Attempt Test
241
+
242
+ ```ruby
243
+ describe "LoginHandler" do
244
+ it "redirects after successful authentication" do
245
+ strategy_result = Otto::Security::Authentication::StrategyResult.new(
246
+ session: { user_id: 123 },
247
+ user: { id: 123 },
248
+ auth_method: 'session', # Auth route succeeded
249
+ metadata: {}
250
+ )
251
+
252
+ expect(strategy_result.authenticated?).to be true
253
+ expect(strategy_result.auth_attempt_succeeded?).to be true
254
+ end
255
+ end
256
+ ```
257
+
258
+ ## Checklist
259
+
260
+ - [ ] Remove all usage of `success?` and `failure?` methods
261
+ - [ ] Update registration flows to use `authenticated?`
262
+ - [ ] Update post-login flows to use `auth_attempt_succeeded?` if needed
263
+ - [ ] Remove manual `StrategyResult` creation in controllers
264
+ - [ ] Update test matchers from `be_success`/`be_failure` to type checks
265
+ - [ ] Verify session contract keys match across apps
266
+ - [ ] Run full test suite: `bundle exec rspec`
267
+ - [ ] Test registration while logged in (should be blocked)
268
+ - [ ] Test login redirect flow (should work correctly)
269
+
270
+ ## Configuration Updates
271
+
272
+ ### Authentication Login Path Configuration
273
+
274
+ When authentication fails for HTML requests, Otto redirects to a login page. You can configure this path:
275
+
276
+ ```ruby
277
+ # Initialize with login_path configuration
278
+ otto = Otto.new do
279
+ auth_config[:login_path] = '/auth/login' # Default: '/signin'
280
+ end
281
+
282
+ # Or configure after initialization
283
+ otto.auth_config[:login_path] = '/custom/login'
284
+ ```
285
+
286
+ **Note:** If not configured, the default fallback is `/signin`. Ensure this route exists or configure your actual login path to avoid 404 errors on authentication failures.
287
+
288
+ ## Additional Improvements in v2.0.0-pre2
289
+
290
+ ### Middleware Architecture Enhancements
291
+
292
+ **1. Renamed MCP ValidationMiddleware → SchemaValidationMiddleware**
293
+ - Resolves naming collision with `Otto::Security::ValidationMiddleware`
294
+ - `Otto::MCP::SchemaValidationMiddleware` now clearly indicates JSON schema validation
295
+ - `Otto::Security::ValidationMiddleware` remains for input sanitization
296
+
297
+ **Migration:**
298
+ ```ruby
299
+ # File renamed: lib/otto/mcp/validation.rb → lib/otto/mcp/schema_validation.rb
300
+ # Class renamed automatically if using Otto's MCP server
301
+ # No action needed for most users
302
+ ```
303
+
304
+ **2. Centralized Env Keys Documentation**
305
+ - New file: `lib/otto/env_keys.rb`
306
+ - Documents all `env['otto.*']` keys with types, setters, and users
307
+ - Includes usage examples and multi-app integration patterns
308
+ - Essential reference for custom middleware development
309
+
310
+ **3. RateLimitMiddleware Clarity**
311
+ - Added documentation clarifying it's a CONFIGURATOR, not enforcer
312
+ - Actual rate limiting happens in Rack::Attack middleware
313
+ - `call` method is explicitly a pass-through
314
+
315
+ **4. Middleware Order Enforcement**
316
+ - New method: `MiddlewareStack#validate_mcp_middleware_order`
317
+ - New method: `MiddlewareStack#add_with_position` for explicit ordering
318
+ - MCP Server uses explicit positioning: `position: :first` and `position: :last`
319
+ - Validates middleware order and warns if suboptimal
320
+ - Optimal: RateLimitMiddleware → TokenMiddleware → SchemaValidationMiddleware
321
+ - Validation runs automatically when MCP is enabled
322
+
323
+ **Usage Example:**
324
+ ```ruby
325
+ # Explicit positioning for clarity
326
+ middleware.add_with_position(
327
+ Otto::MCP::RateLimitMiddleware,
328
+ security_config,
329
+ position: :first # Ensures rate limiting runs first
330
+ )
331
+
332
+ middleware.add_with_position(
333
+ Otto::MCP::SchemaValidationMiddleware,
334
+ position: :last # Ensures validation runs last
335
+ )
336
+ ```
337
+
338
+ ## Questions?
339
+
340
+ Review the comprehensive inline documentation in:
341
+ - `lib/otto/security/authentication/strategy_result.rb` (lines 1-90) - Auth semantics
342
+ - `lib/otto/security/authentication/authentication_middleware.rb` - Auth middleware
343
+ - `lib/otto/env_keys.rb` - Complete env key registry
344
+
345
+ The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.
@@ -0,0 +1 @@
1
+ !**/README.md
@@ -0,0 +1,33 @@
1
+ # Otto - Advanced Routes Example
2
+
3
+ This example demonstrates the advanced routing syntax features available in Otto, such as response type negotiation, CSRF exemptions, and routing to logic classes.
4
+
5
+ ## Project Structure
6
+
7
+ The example is structured to separate concerns, making it easier to navigate:
8
+
9
+ - `config.ru`: The main Rackup file that loads and runs the Otto application.
10
+ - `routes`: A comprehensive file demonstrating various advanced routing syntaxes. This file serves as a good reference.
11
+ - `app.rb`: A simple loader that requires all controller and logic files.
12
+ - `app/controllers/`: Contains the `RoutesApp` class and other namespaced controller modules that handle incoming requests.
13
+ - `app/logic/`: Contains various "Logic Classes". These are special classes that can be routed to directly, encapsulating business logic for a specific route. They are organized into namespaces to show how Otto handles complex class names.
14
+ - `run.rb`, `puma.rb`, `test.rb`: Alternative ways to run or test the application.
15
+
16
+ ## Key Features Demonstrated
17
+
18
+ - **Response Types:** Defining `response=json`, `response=view`, etc., directly in the `routes` file.
19
+ - **CSRF Exemption:** Using `csrf=exempt` for APIs or webhooks.
20
+ - **Logic Classes:** Routing directly to a class (e.g., `GET /logic/simple SimpleLogic`). Otto automatically instantiates it and calls its `process` method.
21
+ - **Namespaced Targets:** Routing to deeply namespaced classes and modules (e.g., `GET /logic/v2/dashboard V2::Logic::Dashboard`).
22
+ - **Custom Parameters:** Adding arbitrary key-value parameters to a route for custom logic.
23
+
24
+ ## Running the Example
25
+
26
+ 1. Make sure you have the necessary gems installed (`bundle install`).
27
+ 2. Run the application from the root of the `otto` project:
28
+
29
+ ```sh
30
+ rackup examples/advanced_routes/config.ru
31
+ ```
32
+
33
+ 3. The application will be running at `http://localhost:9292`. You can use `curl` or your browser to test the various routes defined in the `routes` file.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Handlers
4
+ class Async
5
+ def execute
6
+ [200, { 'content-type' => 'application/json' }, ['{"execution": "async", "csrf": "exempt"}']]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Handlers
4
+ class Dynamic
5
+ def process
6
+ [200, { 'content-type' => 'application/json' }, ['{"handler": "dynamic", "processed": true}']]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Handlers
4
+ class Static
5
+ def self.serve
6
+ [200, { 'content-type' => 'text/html' }, ['<h1>Static Handler</h1><p>Static content served</p>']]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modules
4
+ class Auth
5
+ def process
6
+ [200, { 'content-type' => 'text/html' }, ['<h1>Auth Module</h1><p>Instance method implementation</p>']]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modules
4
+ class Transformer
5
+ def transform
6
+ [200, { 'content-type' => 'application/json' }, ['{"transformation": "complete", "csrf": "exempt"}']]
7
+ end
8
+ end
9
+ end