otto 2.0.0.pre9 → 2.0.0.pre10
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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +19 -0
- data/CLAUDE.md +44 -0
- data/Gemfile.lock +2 -2
- data/lib/otto/core/error_handler.rb +85 -2
- data/lib/otto/core/helper_registry.rb +135 -0
- data/lib/otto/core/lifecycle_hooks.rb +63 -0
- data/lib/otto/core/middleware_management.rb +70 -0
- data/lib/otto/core.rb +3 -0
- data/lib/otto/helpers.rb +2 -2
- data/lib/otto/locale/middleware.rb +1 -1
- data/lib/otto/mcp/core.rb +33 -0
- data/lib/otto/mcp/protocol.rb +1 -1
- data/lib/otto/mcp/schema_validation.rb +1 -1
- data/lib/otto/mcp.rb +1 -0
- data/lib/otto/privacy/core.rb +82 -0
- data/lib/otto/privacy.rb +1 -0
- data/lib/otto/{helpers/request.rb → request.rb} +17 -6
- data/lib/otto/{helpers/response.rb → response.rb} +20 -7
- data/lib/otto/route.rb +2 -4
- data/lib/otto/route_handlers/base.rb +5 -7
- data/lib/otto/route_handlers/lambda.rb +2 -2
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
- data/lib/otto/security/core.rb +167 -0
- data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
- data/lib/otto/security/middleware/validation_middleware.rb +1 -1
- data/lib/otto/security.rb +1 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +24 -443
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5e7fb7a7177b6488a5a3cdacc8e3a929d58f92d44fb03bb506effca3f6c3550
|
|
4
|
+
data.tar.gz: 351a23a9c8e12aff50296cde108e73478dac7cf052d687deb5855f3ccb35cd79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 651edcf79562877a2046132ccc876f747545d1bed8f4821352af4c16f8e795bec5d02e82afcf27320b368af9e25853df3c71d26c4fa6b6f81878704322d0f29d
|
|
7
|
+
data.tar.gz: e763f2d28e2ff4dd0d16a98ff8ab9eda6d495d32e7be10b31a1c21516a075fb1f49a44464eeaf66d92f701821ab3fce91ab72c25d18430efa70e65b2a435cb94
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,25 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.0.0.pre10:
|
|
11
|
+
|
|
12
|
+
2.0.0.pre10 — 2025-12-09
|
|
13
|
+
========================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- ``Otto::Request`` and ``Otto::Response`` classes extending Rack equivalents
|
|
19
|
+
- ``register_request_helpers`` and ``register_response_helpers`` for application-specific helpers
|
|
20
|
+
- Helper modules included at class level (not per-request extension)
|
|
21
|
+
|
|
22
|
+
Changed
|
|
23
|
+
-------
|
|
24
|
+
|
|
25
|
+
- Moved ``lib/otto/helpers/request.rb`` → ``lib/otto/request.rb``
|
|
26
|
+
- Moved ``lib/otto/helpers/response.rb`` → ``lib/otto/response.rb``
|
|
27
|
+
- All internal code now uses ``Otto::Request``/``Otto::Response`` instead of ``Rack::Request``/``Rack::Response``
|
|
28
|
+
|
|
10
29
|
.. _changelog-2.0.0.pre9:
|
|
11
30
|
|
|
12
31
|
2.0.0.pre9 — 2025-12-06
|
data/CLAUDE.md
CHANGED
|
@@ -14,6 +14,40 @@ otto.register_error_handler(YourApp::RateLimited, status: 429, log_level: :warn)
|
|
|
14
14
|
|
|
15
15
|
Must be registered before first request (before configuration freezing).
|
|
16
16
|
|
|
17
|
+
## Request/Response Helper Registration
|
|
18
|
+
|
|
19
|
+
Register application helpers that integrate with Otto features (authentication, privacy, locale):
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
module YourApp::RequestHelpers
|
|
23
|
+
def current_customer
|
|
24
|
+
user = strategy_result&.user
|
|
25
|
+
user.is_a?(YourApp::Customer) ? user : YourApp::Customer.anonymous
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
otto = Otto.new('routes.txt')
|
|
30
|
+
otto.register_request_helpers(YourApp::RequestHelpers)
|
|
31
|
+
otto.register_response_helpers(YourApp::ResponseHelpers)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Must be registered before first request.
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> **Helper availability by context:** The base `Otto::Request` and `Otto::Response` classes (with Otto's built-in helpers like `masked_ip`, `geo_country`) are available everywhere, including Rack middleware. However, custom helpers registered via `register_request_helpers` and `register_response_helpers` are only available in route handlers, error handlers, and components that have access to the Otto instance's dynamic request/response classes.
|
|
38
|
+
|
|
39
|
+
### Reserved Method Names
|
|
40
|
+
|
|
41
|
+
Helper modules should avoid overriding these methods inherited from Rack::Request/Rack::Response:
|
|
42
|
+
|
|
43
|
+
**Request reserved methods**: `env`, `params`, `cookies`, `session`, `path`, `path_info`, `query_string`, `request_method`, `content_type`, `content_length`, `media_type`, `get?`, `post?`, `put?`, `delete?`, `head?`, `options?`, `patch?`, `xhr?`, `referer`, `user_agent`, `base_url`, `url`, `fullpath`, `ip`, `host`, `port`, `ssl?`, `scheme`
|
|
44
|
+
|
|
45
|
+
**Response reserved methods**: `status`, `headers`, `body`, `finish`, `write`, `close`, `set_cookie`, `delete_cookie`, `redirect`, `content_type`, `content_length`, `location`
|
|
46
|
+
|
|
47
|
+
**Otto-specific methods**: `request` (on Response), `app_path`, `masked_ip`, `hashed_ip`, `client_ipaddress`, `secure?`, `local?`, `ajax?`
|
|
48
|
+
|
|
49
|
+
No runtime validation is performed for performance reasons. Overriding these methods will cause undefined behavior.
|
|
50
|
+
|
|
17
51
|
## Authentication Architecture
|
|
18
52
|
|
|
19
53
|
Authentication is handled by `RouteAuthWrapper` at the handler level, NOT by middleware.
|
|
@@ -115,6 +149,16 @@ bundle exec rubocop
|
|
|
115
149
|
bundle exec rspec
|
|
116
150
|
```
|
|
117
151
|
|
|
152
|
+
## Git Commands
|
|
153
|
+
|
|
154
|
+
Always use `--no-pager` with git commands to avoid interactive pager issues:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git --no-pager diff
|
|
158
|
+
git --no-pager log
|
|
159
|
+
git --no-pager show
|
|
160
|
+
```
|
|
161
|
+
|
|
118
162
|
## Key Architecture Principles
|
|
119
163
|
|
|
120
164
|
- **Security by Default**: IP privacy, configuration freezing, backtrace sanitization
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
otto (2.0.0.
|
|
4
|
+
otto (2.0.0.pre10)
|
|
5
5
|
concurrent-ruby (~> 1.3, < 2.0)
|
|
6
6
|
facets (~> 3.1)
|
|
7
7
|
ipaddr (~> 1, < 2.0)
|
|
@@ -113,7 +113,7 @@ GEM
|
|
|
113
113
|
rack
|
|
114
114
|
rack-test (2.2.0)
|
|
115
115
|
rack (>= 1.3)
|
|
116
|
-
rackup (2.
|
|
116
|
+
rackup (2.3.1)
|
|
117
117
|
rack (>= 3)
|
|
118
118
|
rainbow (3.1.1)
|
|
119
119
|
rbs (3.9.5)
|
|
@@ -38,7 +38,7 @@ class Otto
|
|
|
38
38
|
|
|
39
39
|
# Parse request for content negotiation
|
|
40
40
|
begin
|
|
41
|
-
|
|
41
|
+
Otto::Request.new(env)
|
|
42
42
|
rescue StandardError
|
|
43
43
|
nil
|
|
44
44
|
end
|
|
@@ -75,6 +75,89 @@ class Otto
|
|
|
75
75
|
@server_error || secure_error_response(error_id)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Register an error handler for expected business logic errors
|
|
79
|
+
#
|
|
80
|
+
# This allows you to handle known error conditions (like missing resources,
|
|
81
|
+
# expired data, rate limits) without logging them as unhandled 500 errors.
|
|
82
|
+
#
|
|
83
|
+
# @param error_class [Class, String] The exception class or class name to handle
|
|
84
|
+
# @param status [Integer] HTTP status code to return (default: 500)
|
|
85
|
+
# @param log_level [Symbol] Log level for expected errors (:info, :warn, :error)
|
|
86
|
+
# @param handler [Proc] Optional block to customize error response
|
|
87
|
+
#
|
|
88
|
+
# @example Basic usage with status code
|
|
89
|
+
# otto.register_error_handler(Onetime::MissingSecret, status: 404, log_level: :info)
|
|
90
|
+
# otto.register_error_handler(Onetime::SecretExpired, status: 410, log_level: :info)
|
|
91
|
+
#
|
|
92
|
+
# @example With custom response handler
|
|
93
|
+
# otto.register_error_handler(Onetime::RateLimited, status: 429, log_level: :warn) do |error, req|
|
|
94
|
+
# {
|
|
95
|
+
# error: 'Rate limit exceeded',
|
|
96
|
+
# retry_after: error.retry_after,
|
|
97
|
+
# message: error.message
|
|
98
|
+
# }
|
|
99
|
+
# end
|
|
100
|
+
#
|
|
101
|
+
# @example Using string class names (for lazy loading)
|
|
102
|
+
# otto.register_error_handler('Onetime::MissingSecret', status: 404, log_level: :info)
|
|
103
|
+
#
|
|
104
|
+
def register_error_handler(error_class, status: 500, log_level: :info, &handler)
|
|
105
|
+
ensure_not_frozen!
|
|
106
|
+
|
|
107
|
+
# Normalize error class to string for consistent lookup
|
|
108
|
+
error_class_name = error_class.is_a?(String) ? error_class : error_class.name
|
|
109
|
+
|
|
110
|
+
@error_handlers[error_class_name] = {
|
|
111
|
+
status: status,
|
|
112
|
+
log_level: log_level,
|
|
113
|
+
handler: handler
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Register all Otto framework error classes with appropriate status codes
|
|
120
|
+
#
|
|
121
|
+
# This method auto-registers base HTTP error classes and all framework-specific
|
|
122
|
+
# error classes (Security, MCP) so that raising them automatically returns the
|
|
123
|
+
# correct HTTP status code instead of 500.
|
|
124
|
+
#
|
|
125
|
+
# Users can override these registrations by calling register_error_handler
|
|
126
|
+
# after Otto.new with custom status codes or log levels.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
# @api private
|
|
130
|
+
def register_framework_errors
|
|
131
|
+
# Base HTTP errors (for direct use or subclassing by implementing projects)
|
|
132
|
+
register_error_from_class(Otto::NotFoundError)
|
|
133
|
+
register_error_from_class(Otto::BadRequestError)
|
|
134
|
+
register_error_from_class(Otto::UnauthorizedError)
|
|
135
|
+
register_error_from_class(Otto::ForbiddenError)
|
|
136
|
+
register_error_from_class(Otto::PayloadTooLargeError)
|
|
137
|
+
|
|
138
|
+
# Security module errors
|
|
139
|
+
register_error_from_class(Otto::Security::AuthorizationError)
|
|
140
|
+
register_error_from_class(Otto::Security::CSRFError)
|
|
141
|
+
register_error_from_class(Otto::Security::RequestTooLargeError)
|
|
142
|
+
register_error_from_class(Otto::Security::ValidationError)
|
|
143
|
+
|
|
144
|
+
# MCP module errors
|
|
145
|
+
register_error_from_class(Otto::MCP::ValidationError)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Register an error handler using the error class as the single source of truth
|
|
149
|
+
#
|
|
150
|
+
# @param error_class [Class] Error class that responds to default_status and default_log_level
|
|
151
|
+
# @return [void]
|
|
152
|
+
# @api private
|
|
153
|
+
def register_error_from_class(error_class)
|
|
154
|
+
register_error_handler(
|
|
155
|
+
error_class,
|
|
156
|
+
status: error_class.default_status,
|
|
157
|
+
log_level: error_class.default_log_level
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
78
161
|
private
|
|
79
162
|
|
|
80
163
|
# Handle expected business logic errors with custom status codes and logging
|
|
@@ -108,7 +191,7 @@ class Otto
|
|
|
108
191
|
response_body = if handler_config[:handler]
|
|
109
192
|
# Use custom handler block if provided
|
|
110
193
|
begin
|
|
111
|
-
req =
|
|
194
|
+
req = @request_class.new(env)
|
|
112
195
|
result = handler_config[:handler].call(error, req)
|
|
113
196
|
|
|
114
197
|
# Validate that custom handler returned a Hash
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# lib/otto/core/helper_registry.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Core
|
|
7
|
+
# Helper registration module for extending Otto's Request and Response classes.
|
|
8
|
+
# Provides the public API for registering custom helper modules.
|
|
9
|
+
module HelperRegistry
|
|
10
|
+
# Register request helper modules
|
|
11
|
+
#
|
|
12
|
+
# Registered modules are included in Otto::Request at the class level,
|
|
13
|
+
# making custom helpers available alongside Otto's built-in helpers.
|
|
14
|
+
# Must be called before first request (before configuration freezing).
|
|
15
|
+
#
|
|
16
|
+
# This is the official integration point for application-specific helpers
|
|
17
|
+
# that work with Otto internals (strategy_result, privacy features, etc.).
|
|
18
|
+
#
|
|
19
|
+
# @param modules [Module, Array<Module>] Module(s) containing helper methods
|
|
20
|
+
# @example
|
|
21
|
+
# module Onetime::RequestHelpers
|
|
22
|
+
# def current_customer
|
|
23
|
+
# user.is_a?(Onetime::Customer) ? user : Onetime::Customer.anonymous
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def organization
|
|
27
|
+
# @organization ||= strategy_result&.metadata.dig(:organization_context, :organization)
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# otto.register_request_helpers(Onetime::RequestHelpers)
|
|
32
|
+
#
|
|
33
|
+
# @raise [ArgumentError] if module is not a Module
|
|
34
|
+
# @raise [FrozenError] if called after configuration is frozen
|
|
35
|
+
def register_request_helpers(*modules)
|
|
36
|
+
begin
|
|
37
|
+
ensure_not_frozen!
|
|
38
|
+
rescue FrozenError
|
|
39
|
+
raise FrozenError, 'Cannot register request helpers after first request'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
modules.each do |mod|
|
|
43
|
+
unless mod.is_a?(Module)
|
|
44
|
+
raise ArgumentError, "Expected Module, got #{mod.class}"
|
|
45
|
+
end
|
|
46
|
+
@request_helper_modules << mod unless @request_helper_modules.include?(mod)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Re-finalize to include newly registered helpers
|
|
50
|
+
finalize_request_response_classes
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Register response helper modules
|
|
54
|
+
#
|
|
55
|
+
# Registered modules are included in Otto::Response at the class level,
|
|
56
|
+
# making custom helpers available alongside Otto's built-in helpers.
|
|
57
|
+
# Must be called before first request (before configuration freezing).
|
|
58
|
+
#
|
|
59
|
+
# @param modules [Module, Array<Module>] Module(s) containing helper methods
|
|
60
|
+
# @example
|
|
61
|
+
# module Onetime::ResponseHelpers
|
|
62
|
+
# def json_success(data, status: 200)
|
|
63
|
+
# headers['content-type'] = 'application/json'
|
|
64
|
+
# self.status = status
|
|
65
|
+
# write JSON.generate({ success: true, data: data })
|
|
66
|
+
# end
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# otto.register_response_helpers(Onetime::ResponseHelpers)
|
|
70
|
+
#
|
|
71
|
+
# @raise [ArgumentError] if module is not a Module
|
|
72
|
+
# @raise [FrozenError] if called after configuration is frozen
|
|
73
|
+
def register_response_helpers(*modules)
|
|
74
|
+
begin
|
|
75
|
+
ensure_not_frozen!
|
|
76
|
+
rescue FrozenError
|
|
77
|
+
raise FrozenError, 'Cannot register response helpers after first request'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
modules.each do |mod|
|
|
81
|
+
unless mod.is_a?(Module)
|
|
82
|
+
raise ArgumentError, "Expected Module, got #{mod.class}"
|
|
83
|
+
end
|
|
84
|
+
@response_helper_modules << mod unless @response_helper_modules.include?(mod)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Re-finalize to include newly registered helpers
|
|
88
|
+
finalize_request_response_classes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get registered request helper modules (for debugging)
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<Module>] Array of registered request helper modules
|
|
94
|
+
# @api private
|
|
95
|
+
def registered_request_helpers
|
|
96
|
+
@request_helper_modules.dup
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get registered response helper modules (for debugging)
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Module>] Array of registered response helper modules
|
|
102
|
+
# @api private
|
|
103
|
+
def registered_response_helpers
|
|
104
|
+
@response_helper_modules.dup
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Finalize request and response classes with framework and custom helpers
|
|
110
|
+
#
|
|
111
|
+
# This method creates Otto's request and response classes by:
|
|
112
|
+
# 1. Subclassing Otto::Request/Response (which have framework helpers built-in)
|
|
113
|
+
# 2. Including any registered custom helper modules
|
|
114
|
+
#
|
|
115
|
+
# Called during initialization and can be called again if helpers are registered
|
|
116
|
+
# after initialization (before first request).
|
|
117
|
+
#
|
|
118
|
+
# @return [void]
|
|
119
|
+
# @api private
|
|
120
|
+
def finalize_request_response_classes
|
|
121
|
+
# Create request class with framework helpers
|
|
122
|
+
# Otto::Request has all framework helpers as instance methods
|
|
123
|
+
@request_class = Class.new(Otto::Request)
|
|
124
|
+
|
|
125
|
+
# Create response class with framework helpers
|
|
126
|
+
# Otto::Response has all framework helpers as instance methods
|
|
127
|
+
@response_class = Class.new(Otto::Response)
|
|
128
|
+
|
|
129
|
+
# Apply registered custom helpers (framework helpers always come first)
|
|
130
|
+
@request_helper_modules&.each { |mod| @request_class.include(mod) }
|
|
131
|
+
@response_helper_modules&.each { |mod| @response_class.include(mod) }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# lib/otto/core/lifecycle_hooks.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Core
|
|
7
|
+
# Lifecycle hooks module for registering callbacks at various points in request processing.
|
|
8
|
+
# Provides the public API for request completion callbacks.
|
|
9
|
+
module LifecycleHooks
|
|
10
|
+
# Register a callback to be executed after each request completes
|
|
11
|
+
#
|
|
12
|
+
# Instance-level request completion callbacks allow each Otto instance
|
|
13
|
+
# to have its own isolated set of callbacks, preventing duplicate
|
|
14
|
+
# invocations in multi-app architectures (e.g., Rack::URLMap).
|
|
15
|
+
#
|
|
16
|
+
# The callback receives three arguments:
|
|
17
|
+
# - request: Rack::Request object
|
|
18
|
+
# - response: Rack::Response object (wrapping the response tuple)
|
|
19
|
+
# - duration: Request processing duration in microseconds
|
|
20
|
+
#
|
|
21
|
+
# @example Basic usage
|
|
22
|
+
# otto = Otto.new(routes_file)
|
|
23
|
+
# otto.on_request_complete do |req, res, duration|
|
|
24
|
+
# logger.info "Request completed", path: req.path, duration: duration
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example Multi-app architecture
|
|
28
|
+
# # App 1: Core Web Application
|
|
29
|
+
# core_router = Otto.new
|
|
30
|
+
# core_router.on_request_complete do |req, res, duration|
|
|
31
|
+
# logger.info "Core app request", path: req.path
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # App 2: API Application
|
|
35
|
+
# api_router = Otto.new
|
|
36
|
+
# api_router.on_request_complete do |req, res, duration|
|
|
37
|
+
# logger.info "API request", path: req.path
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # Each callback only fires for its respective Otto instance
|
|
41
|
+
#
|
|
42
|
+
# @yield [request, response, duration] Block to execute after each request
|
|
43
|
+
# @yieldparam request [Rack::Request] The request object
|
|
44
|
+
# @yieldparam response [Rack::Response] The response object
|
|
45
|
+
# @yieldparam duration [Integer] Duration in microseconds
|
|
46
|
+
# @return [self] Returns self for method chaining
|
|
47
|
+
# @raise [FrozenError] if called after configuration is frozen
|
|
48
|
+
def on_request_complete(&block)
|
|
49
|
+
ensure_not_frozen!
|
|
50
|
+
@request_complete_callbacks << block if block_given?
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get registered request completion callbacks (for internal use)
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
# @return [Array<Proc>] Array of registered callback blocks
|
|
58
|
+
def request_complete_callbacks
|
|
59
|
+
@request_complete_callbacks
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# lib/otto/core/middleware_management.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Core
|
|
7
|
+
# Middleware management module for building and configuring the Rack middleware stack.
|
|
8
|
+
# Provides the public API for adding middleware and building the application.
|
|
9
|
+
module MiddlewareManagement
|
|
10
|
+
# Builds the middleware application chain
|
|
11
|
+
# Called once at initialization and whenever middleware stack changes
|
|
12
|
+
#
|
|
13
|
+
# IMPORTANT: If you have routes with auth requirements, you MUST add session
|
|
14
|
+
# middleware to your middleware stack BEFORE Otto processes requests.
|
|
15
|
+
#
|
|
16
|
+
# Session middleware is required for RouteAuthWrapper to correctly persist
|
|
17
|
+
# session changes during authentication. Common options include:
|
|
18
|
+
# - Rack::Session::Cookie (requires rack-session gem)
|
|
19
|
+
# - Rack::Session::Pool
|
|
20
|
+
# - Rack::Session::Memcache
|
|
21
|
+
# - Any Rack-compatible session middleware
|
|
22
|
+
#
|
|
23
|
+
# Example:
|
|
24
|
+
# use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
|
25
|
+
# otto = Otto.new('routes.txt')
|
|
26
|
+
#
|
|
27
|
+
def build_app!
|
|
28
|
+
base_app = method(:handle_request)
|
|
29
|
+
@app = @middleware.wrap(base_app, @security_config)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Add middleware to the stack
|
|
33
|
+
#
|
|
34
|
+
# @param middleware [Class] Middleware class to add
|
|
35
|
+
# @param args Additional arguments passed to middleware constructor
|
|
36
|
+
def use(middleware, ...)
|
|
37
|
+
ensure_not_frozen!
|
|
38
|
+
@middleware.add(middleware, ...)
|
|
39
|
+
|
|
40
|
+
# NOTE: If build_app! is triggered during a request (via use() or
|
|
41
|
+
# middleware_stack=), the @app instance variable could be swapped
|
|
42
|
+
# mid-request in a multi-threaded environment.
|
|
43
|
+
|
|
44
|
+
build_app! if @app # Rebuild app if already initialized
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Compatibility method for existing tests
|
|
48
|
+
# @return [Array] List of middleware classes
|
|
49
|
+
def middleware_stack
|
|
50
|
+
@middleware.middleware_list
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Compatibility method for existing tests
|
|
54
|
+
# @param stack [Array] Array of middleware classes
|
|
55
|
+
def middleware_stack=(stack)
|
|
56
|
+
@middleware.clear!
|
|
57
|
+
Array(stack).each { |middleware| @middleware.add(middleware) }
|
|
58
|
+
build_app! if @app # Rebuild app if already initialized
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if a specific middleware is enabled
|
|
62
|
+
#
|
|
63
|
+
# @param middleware_class [Class] Middleware class to check
|
|
64
|
+
# @return [Boolean] true if middleware is in the stack
|
|
65
|
+
def middleware_enabled?(middleware_class)
|
|
66
|
+
@middleware.includes?(middleware_class)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/otto/core.rb
CHANGED
|
@@ -8,3 +8,6 @@ require_relative 'core/configuration'
|
|
|
8
8
|
require_relative 'core/error_handler'
|
|
9
9
|
require_relative 'core/uri_generator'
|
|
10
10
|
require_relative 'core/middleware_stack'
|
|
11
|
+
require_relative 'core/helper_registry'
|
|
12
|
+
require_relative 'core/middleware_management'
|
|
13
|
+
require_relative 'core/lifecycle_hooks'
|
data/lib/otto/helpers.rb
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Request and response helpers are now defined directly in Otto::Request and Otto::Response
|
|
6
|
+
# This file is kept for backward compatibility with require statements
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# lib/otto/mcp/core.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module MCP
|
|
7
|
+
# Core MCP (Model Context Protocol) methods included in the Otto class.
|
|
8
|
+
# Provides the public API for enabling and querying MCP server support.
|
|
9
|
+
module Core
|
|
10
|
+
# Enable MCP (Model Context Protocol) server support
|
|
11
|
+
#
|
|
12
|
+
# @param options [Hash] MCP configuration options
|
|
13
|
+
# @option options [Boolean] :http Enable HTTP endpoint (default: true)
|
|
14
|
+
# @option options [Boolean] :stdio Enable STDIO communication (default: false)
|
|
15
|
+
# @option options [String] :endpoint HTTP endpoint path (default: '/_mcp')
|
|
16
|
+
# @example
|
|
17
|
+
# otto.enable_mcp!(http: true, endpoint: '/api/mcp')
|
|
18
|
+
def enable_mcp!(options = {})
|
|
19
|
+
ensure_not_frozen!
|
|
20
|
+
@mcp_server ||= Otto::MCP::Server.new(self)
|
|
21
|
+
|
|
22
|
+
@mcp_server.enable!(options)
|
|
23
|
+
Otto.logger.info '[MCP] Enabled MCP server' if Otto.debug
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if MCP is enabled
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def mcp_enabled?
|
|
29
|
+
@mcp_server&.enabled?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/otto/mcp/protocol.rb
CHANGED
|
@@ -17,7 +17,7 @@ class Otto
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def handle_request(env)
|
|
20
|
-
request =
|
|
20
|
+
request = @otto.request_class.new(env)
|
|
21
21
|
|
|
22
22
|
unless request.post? && request.content_type&.include?('application/json')
|
|
23
23
|
return error_response(nil, -32_600, 'Invalid Request', 'Only JSON-RPC POST requests supported')
|
data/lib/otto/mcp.rb
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# lib/otto/privacy/core.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Privacy
|
|
7
|
+
# Core privacy configuration methods included in the Otto class.
|
|
8
|
+
# Provides the public API for configuring IP privacy features.
|
|
9
|
+
module Core
|
|
10
|
+
# Disable IP privacy to access original IP addresses
|
|
11
|
+
#
|
|
12
|
+
# IMPORTANT: By default, Otto masks public IP addresses for privacy.
|
|
13
|
+
# Private/localhost IPs (127.0.0.0/8, 10.0.0.0/8, etc.) are never masked.
|
|
14
|
+
# Only disable this if you need access to original public IPs.
|
|
15
|
+
#
|
|
16
|
+
# When disabled:
|
|
17
|
+
# - env['REMOTE_ADDR'] contains the real IP address
|
|
18
|
+
# - env['otto.original_ip'] also contains the real IP
|
|
19
|
+
# - No PrivateFingerprint is created
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# otto.disable_ip_privacy!
|
|
23
|
+
def disable_ip_privacy!
|
|
24
|
+
ensure_not_frozen!
|
|
25
|
+
@security_config.ip_privacy_config.disable!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Enable full IP privacy (mask ALL IPs including private/localhost)
|
|
29
|
+
#
|
|
30
|
+
# By default, Otto exempts private and localhost IPs from masking for
|
|
31
|
+
# better development experience. Call this method to mask ALL IPs
|
|
32
|
+
# regardless of type.
|
|
33
|
+
#
|
|
34
|
+
# @example Enable full privacy (mask all IPs)
|
|
35
|
+
# otto = Otto.new(routes_file)
|
|
36
|
+
# otto.enable_full_ip_privacy!
|
|
37
|
+
# # Now 127.0.0.1 → 127.0.0.0, 192.168.1.100 → 192.168.1.0
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @raise [FrozenError] if called after configuration is frozen
|
|
41
|
+
def enable_full_ip_privacy!
|
|
42
|
+
ensure_not_frozen!
|
|
43
|
+
@security_config.ip_privacy_config.mask_private_ips = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Configure IP privacy settings
|
|
47
|
+
#
|
|
48
|
+
# Privacy is enabled by default. Use this method to customize privacy
|
|
49
|
+
# behavior without disabling it entirely.
|
|
50
|
+
#
|
|
51
|
+
# @param octet_precision [Integer] Number of octets to mask (1 or 2, default: 1)
|
|
52
|
+
# @param hash_rotation [Integer] Seconds between key rotation (default: 86400)
|
|
53
|
+
# @param geo [Boolean] Enable geo-location resolution (default: true)
|
|
54
|
+
# @param redis [Redis] Redis connection for multi-server atomic key generation
|
|
55
|
+
#
|
|
56
|
+
# @example Mask 2 octets instead of 1
|
|
57
|
+
# otto.configure_ip_privacy(octet_precision: 2)
|
|
58
|
+
#
|
|
59
|
+
# @example Disable geo-location
|
|
60
|
+
# otto.configure_ip_privacy(geo: false)
|
|
61
|
+
#
|
|
62
|
+
# @example Custom hash rotation
|
|
63
|
+
# otto.configure_ip_privacy(hash_rotation: 24.hours)
|
|
64
|
+
#
|
|
65
|
+
# @example Multi-server with Redis
|
|
66
|
+
# redis = Redis.new(url: ENV['REDIS_URL'])
|
|
67
|
+
# otto.configure_ip_privacy(redis: redis)
|
|
68
|
+
def configure_ip_privacy(octet_precision: nil, hash_rotation: nil, geo: nil, redis: nil)
|
|
69
|
+
ensure_not_frozen!
|
|
70
|
+
config = @security_config.ip_privacy_config
|
|
71
|
+
|
|
72
|
+
config.octet_precision = octet_precision if octet_precision
|
|
73
|
+
config.hash_rotation_period = hash_rotation if hash_rotation
|
|
74
|
+
config.geo_enabled = geo unless geo.nil?
|
|
75
|
+
config.instance_variable_set(:@redis, redis) if redis
|
|
76
|
+
|
|
77
|
+
# Validate configuration
|
|
78
|
+
config.validate!
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|