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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a99ac23967768247096e9bb9664a9efcc0452ea485496389ff3909209fd12d6c
4
- data.tar.gz: 209178d26d3b46ddade717d471c8b10755ea6e79a43b8522a5f1d10dfca3d935
3
+ metadata.gz: d5e7fb7a7177b6488a5a3cdacc8e3a929d58f92d44fb03bb506effca3f6c3550
4
+ data.tar.gz: 351a23a9c8e12aff50296cde108e73478dac7cf052d687deb5855f3ccb35cd79
5
5
  SHA512:
6
- metadata.gz: 6d7a07ad3b7b22dbae59038b9edaf26f5db11f041ae808e3c9430a862956a6e874c47afeae3731f1eff7cb065cb38c125c688ea54900e745288d2e17c4462416
7
- data.tar.gz: 42867d3c6a0d1eb17c440e9b854c826abf5bc0c48b72fa5e76053b00926972a9ba18586f5fbb71f85cb6e5e54426e2c25fff30d77c865fcc21a0e24ab88233de
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.pre9)
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.2.1)
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
- Rack::Request.new(env)
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 = Rack::Request.new(env)
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
- require_relative 'helpers/request'
6
- require_relative 'helpers/response'
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
@@ -68,7 +68,7 @@ class Otto
68
68
  # @return [String] Resolved locale code
69
69
  def detect_locale(env)
70
70
  # 1. Check URL parameter
71
- req = Rack::Request.new(env)
71
+ req = Otto::Request.new(env)
72
72
  locale = req.params['locale']
73
73
  return locale if valid_locale?(locale)
74
74
 
@@ -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
@@ -17,7 +17,7 @@ class Otto
17
17
  end
18
18
 
19
19
  def handle_request(env)
20
- request = Rack::Request.new(env)
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')
@@ -78,7 +78,7 @@ class Otto
78
78
  # Only validate MCP endpoints
79
79
  return @app.call(env) unless mcp_endpoint?(env)
80
80
 
81
- request = Rack::Request.new(env)
81
+ request = Otto::Request.new(env)
82
82
 
83
83
  if request.post? && request.content_type&.include?('application/json')
84
84
  begin
data/lib/otto/mcp.rb CHANGED
@@ -2,4 +2,5 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'mcp/core'
5
6
  require_relative 'mcp/server'
@@ -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
data/lib/otto/privacy.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'privacy/core'
5
6
  require_relative 'privacy/config'
6
7
  require_relative 'privacy/ip_privacy'
7
8
  require_relative 'privacy/geo_resolver'