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
@@ -1,7 +1,8 @@
1
- # examples/basic/app.rb (Streamlined with Design System)
1
+ # examples/basic/app.rb
2
2
 
3
3
  require_relative '../../lib/otto/design_system'
4
4
 
5
+ # Basic example application demonstrating Otto framework features.
5
6
  class App
6
7
  include Otto::DesignSystem
7
8
 
@@ -37,42 +38,13 @@ class App
37
38
  message = req.params['msg']&.strip
38
39
 
39
40
  content = if message.nil? || message.empty?
40
- otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
41
- else
42
- otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
43
- otto_card('Your Message') { otto_code_block(message, 'text') }
44
- end
41
+ otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
42
+ else
43
+ otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
44
+ otto_card('Your Message') { otto_code_block(message, 'text') }
45
+ end
45
46
 
46
47
  content += "<p>#{otto_link('← Back', '/')}</p>"
47
48
  res.body = otto_page(content, 'Feedback')
48
49
  end
49
-
50
- def redirect
51
- res.redirect '/robots.txt'
52
- end
53
-
54
- def robots_text
55
- res.headers['content-type'] = 'text/plain'
56
- res.body = ['User-agent: *', 'Disallow: /private'].join($/)
57
- end
58
-
59
- def display_product
60
- res.headers['content-type'] = 'application/json; charset=utf-8'
61
- prodid = req.params[:prodid]
62
- res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
63
- end
64
-
65
- def not_found
66
- res.status = 404
67
- content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
68
- content += "<p>#{otto_link('← Home', '/')}</p>"
69
- res.body = otto_page(content, '404')
70
- end
71
-
72
- def server_error
73
- res.status = 500
74
- content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
75
- content += "<p>#{otto_link('← Home', '/')}</p>"
76
- res.body = otto_page(content, '500')
77
- end
78
50
  end
@@ -9,12 +9,3 @@
9
9
 
10
10
  GET / App#index
11
11
  POST / App#receive_feedback
12
- GET /redirect App#redirect
13
- GET /robots.txt App#robots_text
14
-
15
- GET /bogus App#no_such_method
16
-
17
- # You can also define these handlers when no
18
- # route can be found or there's a server error. (optional)
19
- GET /404 App#not_found
20
- GET /500 App#server_error
@@ -0,0 +1,87 @@
1
+ # Otto MCP Demo
2
+
3
+ This example demonstrates Otto's Model-Controller-Protocol (MCP) feature. MCP provides a standardized JSON-RPC 2.0 endpoint (`/_mcp`) that allows you to expose application resources and tools securely.
4
+
5
+ This is useful for building CLIs, admin interfaces, or allowing other services to interact with your application programmatically.
6
+
7
+ ## Features Demonstrated
8
+
9
+ * **MCP Endpoint:** A single `POST /_mcp` endpoint for all API interactions.
10
+ * **Authentication:** Requests to the MCP endpoint are protected by bearer token authentication.
11
+ * **Rate Limiting:** The endpoint has its own rate limiting to prevent abuse.
12
+ * **Resources:** Read-only data exposed via `MCP` routes (e.g., listing users).
13
+ * **Tools:** Actions or operations exposed via `TOOL` routes (e.g., creating a user).
14
+
15
+ ## How to Run
16
+
17
+ 1. Make sure you have `bundler` and `thin` installed:
18
+ ```sh
19
+ gem install bundler thin
20
+ ```
21
+
22
+ 2. Install the dependencies from the root of the project:
23
+ ```sh
24
+ bundle install
25
+ ```
26
+
27
+ 3. Start the server from this directory (`examples/mcp_demo`):
28
+ ```sh
29
+ thin -R config.ru -p 9292 start
30
+ ```
31
+ *Note: This demo uses port 9292 as is conventional for Rack apps.*
32
+
33
+ 4. Open your browser and navigate to `http://localhost:9292` to see the welcome page.
34
+
35
+ ## Interacting with the MCP Endpoint
36
+
37
+ All interactions happen via `POST` requests to `http://localhost:9292/_mcp`. You must provide an `Authorization: Bearer <token>` header and a `Content-Type: application/json` header.
38
+
39
+ Valid tokens are `demo-token-123` and `another-token-456`.
40
+
41
+ ### MCP: Initialize
42
+
43
+ The `initialize` method is a built-in MCP method that returns information about the available resources and tools.
44
+
45
+ ```sh
46
+ curl -X POST http://localhost:9292/_mcp \
47
+ -H 'Authorization: Bearer demo-token-123' \
48
+ -H 'Content-Type: application/json' \
49
+ -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'
50
+ ```
51
+
52
+ ### Resource: List Users
53
+
54
+ This calls the `UserAPI.mcp_list_users` method defined as an `MCP` route. The method name for the JSON-RPC call is derived from the route path (`/users` -> `users/list`).
55
+
56
+ ```sh
57
+ curl -X POST http://localhost:9292/_mcp \
58
+ -H 'Authorization: Bearer demo-token-123' \
59
+ -H 'Content-Type: application/json' \
60
+ -d '{"jsonrpc":"2.0","method":"users/list","id":2}'
61
+ ```
62
+
63
+ ### Tool: Create User
64
+
65
+ This calls the `UserAPI.mcp_create_user` method defined as a `TOOL` route. The method name is derived from the route path (`/create_user` -> `create_user`).
66
+
67
+ ```sh
68
+ curl -X POST http://localhost:9292/_mcp \
69
+ -H 'Authorization: Bearer demo-token-123' \
70
+ -H 'Content-Type: application/json' \
71
+ -d '{
72
+ "jsonrpc": "2.0",
73
+ "method": "create_user",
74
+ "id": 3,
75
+ "params": {
76
+ "name": "Charlie",
77
+ "email": "charlie@example.com"
78
+ }
79
+ }'
80
+ ```
81
+
82
+ ## File Structure
83
+
84
+ * `README.md`: This file.
85
+ * `app.rb`: Contains the application logic, including the `DemoApp` for web pages and the `UserAPI` for MCP handlers.
86
+ * `config.ru`: The Rack configuration file. It loads the Otto framework, enables MCP, and runs the application.
87
+ * `routes`: Defines the standard web routes as well as the `MCP` and `TOOL` routes for the MCP endpoint.
@@ -1,22 +1,41 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Example Otto application with MCP support
4
- # This demonstrates Phase 1 & 2 implementation
1
+ # examples/mcp_demo/app.rb
2
+ require 'json'
3
+ require 'time'
4
+
5
+ # DemoApp provides basic HTML pages for the demo.
6
+ class DemoApp
7
+ def self.index(_req, res)
8
+ res.headers['content-type'] = 'text/html; charset=utf-8'
9
+ res.body = <<~HTML
10
+ <h1>Otto MCP Demo</h1>
11
+ <p>This example demonstrates Otto's Model-Controller-Protocol (MCP) feature, which provides a JSON-RPC 2.0 endpoint for interacting with your application.</p>
12
+ <p>The MCP endpoint is available at: <code>POST /_mcp</code></p>
13
+ <p>See the <code>README.md</code> file for detailed `curl` commands to test the API.</p>
14
+ HTML
15
+ end
5
16
 
6
- require_relative '../../lib/otto'
17
+ def self.health(_req, res)
18
+ res.headers['content-type'] = 'text/plain'
19
+ res.body = 'OK'
20
+ end
21
+ end
7
22
 
23
+ # UserAPI provides handlers for the MCP tool and resource routes.
8
24
  class UserAPI
25
+ # MCP Resource: mcp_list_users
26
+ # Accessible via JSON-RPC method "users/list"
9
27
  def self.mcp_list_users
10
28
  {
11
29
  users: [
12
30
  { id: 1, name: 'Alice', email: 'alice@example.com' },
13
- { id: 2, name: 'Bob', email: 'bob@example.com' }
14
- ]
31
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
32
+ ],
15
33
  }.to_json
16
34
  end
17
35
 
18
- def self.mcp_create_user(arguments, env)
19
- # Tool handler that creates a user
36
+ # MCP Tool: mcp_create_user
37
+ # Accessible via JSON-RPC method "create_user"
38
+ def self.mcp_create_user(arguments, _env)
20
39
  name = arguments['name'] || 'Anonymous'
21
40
  email = arguments['email'] || "#{name.downcase}@example.com"
22
41
 
@@ -24,33 +43,9 @@ class UserAPI
24
43
  id: rand(1000..9999),
25
44
  name: name,
26
45
  email: email,
27
- created_at: Time.now.iso8601
46
+ created_at: Time.now.iso8601,
28
47
  }
29
48
 
30
49
  "Created user: #{new_user.to_json}"
31
50
  end
32
51
  end
33
-
34
- # Initialize Otto with MCP support
35
- otto = Otto.new('routes', {
36
- mcp_enabled: true,
37
- auth_tokens: ['demo-token-123'], # Simple token auth
38
- requests_per_minute: 10, # Lower for demo
39
- tools_per_minute: 5
40
- })
41
-
42
- # Enable MCP with authentication tokens
43
- otto.enable_mcp!({
44
- auth_tokens: ['demo-token-123', 'another-token-456'],
45
- enable_validation: true,
46
- enable_rate_limiting: true
47
- })
48
-
49
- puts "Otto MCP Demo Server starting..."
50
- puts "MCP endpoint: POST /_mcp"
51
- puts "Auth tokens: demo-token-123, another-token-456"
52
- puts "Usage: curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\"
53
- puts " -d '{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":1,\"params\":{}}' \\"
54
- puts " http://localhost:9292/_mcp"
55
-
56
- otto
@@ -1,68 +1,17 @@
1
- #!/usr/bin/env ruby
1
+ # examples/mcp_demo/config.ru
2
2
 
3
3
  require_relative '../../lib/otto'
4
-
5
- class DemoApp
6
- def self.index(req, res)
7
- res.body = <<-HTML
8
- <h1>Otto MCP Demo</h1>
9
- <p>MCP endpoint available at: <code>POST /_mcp</code></p>
10
- <p>Auth tokens: <code>demo-token-123</code>, <code>another-token-456</code></p>
11
-
12
- <h2>Test MCP Initialize</h2>
13
- <pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
14
- -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' \\
15
- http://localhost:9292/_mcp</pre>
16
-
17
- <h2>List Resources</h2>
18
- <pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
19
- -d '{"jsonrpc":"2.0","method":"resources/list","id":2}' \\
20
- http://localhost:9292/_mcp</pre>
21
-
22
- <h2>List Tools</h2>
23
- <pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
24
- -d '{"jsonrpc":"2.0","method":"tools/list","id":3}' \\
25
- http://localhost:9292/_mcp</pre>
26
- HTML
27
- end
28
-
29
- def self.health(req, res)
30
- res.body = 'OK'
31
- end
32
- end
33
-
34
- class UserAPI
35
- def self.mcp_list_users
36
- {
37
- users: [
38
- { id: 1, name: 'Alice', email: 'alice@example.com' },
39
- { id: 2, name: 'Bob', email: 'bob@example.com' }
40
- ]
41
- }.to_json
42
- end
43
-
44
- def self.mcp_create_user(arguments, env)
45
- # Tool handler that creates a user
46
- name = arguments['name'] || 'Anonymous'
47
- email = arguments['email'] || "#{name.downcase}@example.com"
48
-
49
- new_user = {
50
- id: rand(1000..9999),
51
- name: name,
52
- email: email,
53
- created_at: Time.now.iso8601
54
- }
55
-
56
- "Created user: #{new_user.to_json}"
57
- end
58
- end
4
+ require_relative 'app'
59
5
 
60
6
  # Initialize Otto with MCP support
61
- otto = Otto.new('routes', {
7
+ app = Otto.new('routes', {
62
8
  mcp_enabled: true,
63
9
  auth_tokens: ['demo-token-123', 'another-token-456'],
64
- requests_per_minute: 60,
65
- tools_per_minute: 20
10
+ requests_per_minute: 60, # Rate limiting for the MCP endpoint
11
+ tools_per_minute: 20,
66
12
  })
67
13
 
68
- run otto
14
+ # The `mcp_enabled: true` flag automatically sets up the /_mcp endpoint.
15
+ # The routes file maps MCP and TOOL methods to classes.
16
+
17
+ run app
@@ -0,0 +1,46 @@
1
+ # Otto Security Features Example
2
+
3
+ This example application demonstrates the built-in security features of the Otto framework. It is configured to be secure by default and provides a showcase of best practices.
4
+
5
+ ## Security Features Demonstrated
6
+
7
+ * **CSRF Protection:** All POST forms are protected with a CSRF token to prevent cross-site request forgery attacks.
8
+ * **Input Validation:** All user-submitted data is validated on the server-side for length and content, preventing common injection attacks.
9
+ * **XSS Prevention:** All output is properly escaped to prevent cross-site scripting (XSS). You can test this by submitting `<script>alert('XSS')</script>` in any form.
10
+ * **Secure File Uploads:** File uploads are validated, and filenames are sanitized to prevent directory traversal and other file-based attacks.
11
+ * **Security Headers:** The application sends important security headers like `Content-Security-Policy`, `Strict-Transport-Security`, and `X-Frame-Options`.
12
+ * **Request Limiting:** The application is configured to limit the maximum request size, parameter depth, and number of parameter keys to prevent denial-of-service attacks.
13
+ * **Trusted Proxies:** The configuration includes a list of trusted proxy servers, ensuring that `X-Forwarded-*` headers are handled correctly and securely.
14
+
15
+ ## How to Run
16
+
17
+ 1. Make sure you have `bundler` and `thin` installed:
18
+ ```sh
19
+ gem install bundler thin
20
+ ```
21
+
22
+ 2. Install the dependencies from the root of the project:
23
+ ```sh
24
+ bundle install
25
+ ```
26
+
27
+ 3. Start the server from this directory (`examples/security_features`):
28
+ ```sh
29
+ thin -e dev -R config.ru -p 10770 start
30
+ ```
31
+
32
+ 4. Open your browser and navigate to `http://localhost:10770`.
33
+
34
+ ## What to Test
35
+
36
+ * **XSS Protection:** Try entering `<script>alert("XSS")</script>` into any of the form fields. You will see that the input is safely displayed as text instead of being executed as a script.
37
+ * **Input Validation:** Try submitting a very long message in the feedback form to see the length validation in action.
38
+ * **File Uploads:** Try uploading different types of files. The application will show you how it sanitizes the filename.
39
+ * **Security Headers:** Open your browser's developer tools and inspect the network requests. You will see the security headers in the response. You can also visit the `/headers` path to see a JSON representation of the request headers your browser is sending.
40
+
41
+ ## File Structure
42
+
43
+ * `README.md`: This file.
44
+ * `app.rb`: The main application logic, demonstrating how to handle forms, file uploads, and user input in a secure way.
45
+ * `config.ru`: The Rack configuration file. This is where the security features are enabled and configured for the Otto application.
46
+ * `routes`: Defines the URL routes and maps them to methods in the `SecureApp` class.
@@ -1,7 +1,8 @@
1
- # examples/security_features/app.rb (Updated with Design System)
1
+ # examples/security_features/app.rb
2
2
 
3
3
  require_relative '../../lib/otto/design_system'
4
4
 
5
+ # Example application demonstrating Otto's security features including CSRF protection
5
6
  class SecureApp
6
7
  include Otto::DesignSystem
7
8
  include Otto::Security::CSRFHelpers
@@ -106,18 +107,18 @@ class SecureApp
106
107
  end
107
108
 
108
109
  content = if safe_message.empty?
109
- otto_alert('error', 'Validation Error', 'Message cannot be empty.')
110
- else
111
- <<~HTML
112
- #{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
113
-
114
- #{otto_card('Your Message') do
115
- otto_code_block(safe_message, 'text')
116
- end}
117
- HTML
110
+ otto_alert('error', 'Validation Error', 'Message cannot be empty.')
111
+ else
112
+ <<~HTML
113
+ #{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
114
+
115
+ #{otto_card('Your Message') do
116
+ otto_code_block(safe_message, 'text')
117
+ end}
118
+ HTML
118
119
  end
119
- rescue Otto::Security::ValidationError => ex
120
- content = otto_alert('error', 'Security Validation Failed', ex.message)
120
+ rescue Otto::Security::ValidationError => e
121
+ content = otto_alert('error', 'Security Validation Failed', e.message)
121
122
  rescue StandardError
122
123
  content = otto_alert('error', 'Processing Error', 'An error occurred processing your request.')
123
124
  end
@@ -135,18 +136,18 @@ class SecureApp
135
136
  else
136
137
  begin
137
138
  filename = begin
138
- uploaded_file[:filename]
139
+ uploaded_file[:filename]
139
140
  rescue StandardError
140
- uploaded_file.original_filename
141
+ uploaded_file.original_filename
141
142
  end
142
143
  rescue StandardError
143
144
  'unknown'
144
145
  end
145
146
 
146
147
  safe_filename = if respond_to?(:sanitize_filename)
147
- sanitize_filename(filename)
148
- else
149
- File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
148
+ sanitize_filename(filename)
149
+ else
150
+ File.basename(filename.to_s).gsub(/[^\w\-_.]/, '_')
150
151
  end
151
152
 
152
153
  file_info = {
@@ -173,8 +174,8 @@ class SecureApp
173
174
  </div>
174
175
  HTML
175
176
  end
176
- rescue Otto::Security::ValidationError => ex
177
- content = otto_alert('error', 'File Validation Failed', ex.message)
177
+ rescue Otto::Security::ValidationError => e
178
+ content = otto_alert('error', 'File Validation Failed', e.message)
178
179
  rescue StandardError
179
180
  content = otto_alert('error', 'Upload Error', 'An error occurred during file upload.')
180
181
  end
@@ -199,9 +200,7 @@ class SecureApp
199
200
  safe_bio = bio.to_s.strip[0..499]
200
201
  end
201
202
 
202
- unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
203
- raise Otto::Security::ValidationError, 'Invalid email format'
204
- end
203
+ raise Otto::Security::ValidationError, 'Invalid email format' unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
205
204
 
206
205
  profile_data = {
207
206
  'Name' => safe_name,
@@ -221,8 +220,8 @@ class SecureApp
221
220
  profile_html
222
221
  end}
223
222
  HTML
224
- rescue Otto::Security::ValidationError => ex
225
- content = otto_alert('error', 'Profile Validation Failed', ex.message)
223
+ rescue Otto::Security::ValidationError => e
224
+ content = otto_alert('error', 'Profile Validation Failed', e.message)
226
225
  rescue StandardError
227
226
  content = otto_alert('error', 'Update Error', 'An error occurred updating your profile.')
228
227
  end
@@ -14,8 +14,8 @@ require_relative 'app'
14
14
 
15
15
  # Create Otto app with security features enabled
16
16
  app = Otto.new('./routes', {
17
- # Enable CSRF protection for POST, PUT, DELETE requests
18
- csrf_protection: true,
17
+ # Enable CSRF protection for POST, PUT, DELETE requests
18
+ csrf_protection: true,
19
19
 
20
20
  # Enable input validation and sanitization
21
21
  request_validation: true,
@@ -44,13 +44,12 @@ app = Otto.new('./routes', {
44
44
  'strict-transport-security' => 'max-age=31536000; includeSubDomains',
45
45
  'x-frame-options' => 'DENY',
46
46
  },
47
- }
48
- )
47
+ })
49
48
 
50
49
  # Optional: Configure additional security settings
51
- app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
52
- app.security_config.max_param_depth = 10 # Limit parameter nesting
53
- app.security_config.max_param_keys = 50 # Limit parameters per request
50
+ app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
51
+ app.security_config.max_param_depth = 10 # Limit parameter nesting
52
+ app.security_config.max_param_keys = 50 # Limit parameters per request
54
53
 
55
54
  # Optional: Add static file serving with security
56
55
  app.option[:public] = public_path
@@ -62,10 +61,9 @@ if ENV['RACK_ENV'] == 'production'
62
61
 
63
62
  # More restrictive CSP for production
64
63
  app.set_security_headers({
65
- 'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
64
+ 'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
66
65
  'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload',
67
- },
68
- )
66
+ })
69
67
  else
70
68
  # Development-specific settings
71
69
  puts '🔒 Security features enabled:'
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/core/configuration.rb
4
+
5
+ require_relative '../security/csrf'
6
+ require_relative '../security/validator'
7
+ require_relative '../security/authentication'
8
+ require_relative '../security/rate_limiting'
9
+ require_relative '../mcp/server'
10
+
11
+ class Otto
12
+ module Core
13
+ # Configuration module providing locale and application configuration methods
14
+ module Configuration
15
+ def configure_locale(opts)
16
+ # Start with global configuration
17
+ global_config = self.class.global_config
18
+ @locale_config = nil
19
+
20
+ # Check if we have any locale configuration from any source
21
+ has_global_locale = global_config && (global_config[:available_locales] || global_config[:default_locale])
22
+ has_direct_options = opts[:available_locales] || opts[:default_locale]
23
+ has_legacy_config = opts[:locale_config]
24
+
25
+ # Only create locale_config if we have configuration from somewhere
26
+ return unless has_global_locale || has_direct_options || has_legacy_config
27
+
28
+ @locale_config = {}
29
+
30
+ # Apply global configuration first
31
+ if global_config && global_config[:available_locales]
32
+ @locale_config[:available_locales] =
33
+ global_config[:available_locales]
34
+ end
35
+ if global_config && global_config[:default_locale]
36
+ @locale_config[:default_locale] =
37
+ global_config[:default_locale]
38
+ end
39
+
40
+ # Apply direct instance options (these override global config)
41
+ @locale_config[:available_locales] = opts[:available_locales] if opts[:available_locales]
42
+ @locale_config[:default_locale] = opts[:default_locale] if opts[:default_locale]
43
+
44
+ # Legacy support: Configure locale if provided in initialization options via locale_config hash
45
+ return unless opts[:locale_config]
46
+
47
+ locale_opts = opts[:locale_config]
48
+ if locale_opts[:available_locales] || locale_opts[:available]
49
+ @locale_config[:available_locales] =
50
+ locale_opts[:available_locales] || locale_opts[:available]
51
+ end
52
+ return unless locale_opts[:default_locale] || locale_opts[:default]
53
+
54
+ @locale_config[:default_locale] =
55
+ locale_opts[:default_locale] || locale_opts[:default]
56
+ end
57
+
58
+ def configure_security(opts)
59
+ # Enable CSRF protection if requested
60
+ enable_csrf_protection! if opts[:csrf_protection]
61
+
62
+ # Enable request validation if requested
63
+ enable_request_validation! if opts[:request_validation]
64
+
65
+ # Enable rate limiting if requested
66
+ if opts[:rate_limiting]
67
+ rate_limiting_opts = opts[:rate_limiting].is_a?(Hash) ? opts[:rate_limiting] : {}
68
+ enable_rate_limiting!(rate_limiting_opts)
69
+ end
70
+
71
+ # Add trusted proxies if provided
72
+ Array(opts[:trusted_proxies]).each { |proxy| add_trusted_proxy(proxy) } if opts[:trusted_proxies]
73
+
74
+ # Set custom security headers
75
+ return unless opts[:security_headers]
76
+
77
+ set_security_headers(opts[:security_headers])
78
+ end
79
+
80
+ def configure_authentication(opts)
81
+ # Update existing @auth_config rather than creating a new one
82
+ # to maintain synchronization with the configurator
83
+ @auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
84
+ @auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]
85
+
86
+ # Enable authentication middleware if strategies are configured
87
+ return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
88
+
89
+ enable_authentication!
90
+ end
91
+
92
+ def configure_mcp(opts)
93
+ @mcp_server = nil
94
+
95
+ # Enable MCP if requested in options
96
+ return unless opts[:mcp_enabled] || opts[:mcp_http] || opts[:mcp_stdio]
97
+
98
+ @mcp_server = Otto::MCP::Server.new(self)
99
+
100
+ mcp_options = {}
101
+ mcp_options[:http_endpoint] = opts[:mcp_endpoint] if opts[:mcp_endpoint]
102
+
103
+ return unless opts[:mcp_http] != false # Default to true unless explicitly disabled
104
+
105
+ @mcp_server.enable!(mcp_options)
106
+ end
107
+
108
+ # Configure locale settings for the application
109
+ #
110
+ # @param available_locales [Hash] Hash of available locales (e.g., { 'en' => 'English', 'es' => 'Spanish' })
111
+ # @param default_locale [String] Default locale to use as fallback
112
+ # @example
113
+ # otto.configure(
114
+ # available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
115
+ # default_locale: 'en'
116
+ # )
117
+ def configure(available_locales: nil, default_locale: nil)
118
+ @locale_config ||= {}
119
+ @locale_config[:available_locales] = available_locales if available_locales
120
+ @locale_config[:default_locale] = default_locale if default_locale
121
+ end
122
+
123
+ # Configure rate limiting settings.
124
+ #
125
+ # @param config [Hash] Rate limiting configuration
126
+ # @option config [Integer] :requests_per_minute Maximum requests per minute per IP
127
+ # @option config [Hash] :custom_rules Hash of custom rate limiting rules
128
+ # @option config [Object] :cache_store Custom cache store for rate limiting
129
+ # @example
130
+ # otto.configure_rate_limiting({
131
+ # requests_per_minute: 50,
132
+ # custom_rules: {
133
+ # 'api_calls' => { limit: 30, period: 60, condition: ->(req) { req.path.start_with?('/api') }}
134
+ # }
135
+ # })
136
+ def configure_rate_limiting(config)
137
+ @security_config.rate_limiting_config.merge!(config)
138
+ end
139
+
140
+ # Configure authentication strategies for route-level access control.
141
+ #
142
+ # @param strategies [Hash] Hash mapping strategy names to strategy instances
143
+ # @param default_strategy [String] Default strategy to use when none specified
144
+ # @example
145
+ # otto.configure_auth_strategies({
146
+ # 'noauth' => Otto::Security::Authentication::Strategies::NoAuthStrategy.new,
147
+ # 'authenticated' => Otto::Security::Authentication::Strategies::SessionStrategy.new(session_key: 'user_id'),
148
+ # 'role:admin' => Otto::Security::Authentication::Strategies::RoleStrategy.new(['admin']),
149
+ # 'api_key' => Otto::Security::Authentication::Strategies::APIKeyStrategy.new(api_keys: ['secret123'])
150
+ # })
151
+ def configure_auth_strategies(strategies, default_strategy: 'noauth')
152
+ # Update existing @auth_config rather than creating a new one
153
+ @auth_config[:auth_strategies] = strategies
154
+ @auth_config[:default_auth_strategy] = default_strategy
155
+
156
+ enable_authentication! unless strategies.empty?
157
+ end
158
+
159
+ private
160
+
161
+ def middleware_enabled?(middleware_class)
162
+ # Only check the new middleware stack as the single source of truth
163
+ @middleware && @middleware.includes?(middleware_class)
164
+ end
165
+ end
166
+ end
167
+ end