otto 2.0.0.pre3 → 2.0.0.pre7

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +74 -540
  10. data/Gemfile +4 -2
  11. data/Gemfile.lock +58 -19
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/examples/advanced_routes/README.md +137 -20
  16. data/examples/authentication_strategies/README.md +212 -19
  17. data/examples/backtrace_sanitization_demo.rb +86 -0
  18. data/examples/basic/README.md +61 -10
  19. data/examples/error_handler_registration.rb +136 -0
  20. data/examples/logging_improvements.rb +76 -0
  21. data/examples/mcp_demo/README.md +187 -27
  22. data/examples/security_features/README.md +249 -30
  23. data/examples/simple_geo_resolver.rb +107 -0
  24. data/lib/otto/core/configuration.rb +15 -20
  25. data/lib/otto/core/error_handler.rb +138 -8
  26. data/lib/otto/core/file_safety.rb +2 -2
  27. data/lib/otto/core/freezable.rb +2 -2
  28. data/lib/otto/core/middleware_stack.rb +2 -2
  29. data/lib/otto/core/router.rb +61 -8
  30. data/lib/otto/core/uri_generator.rb +2 -2
  31. data/lib/otto/core.rb +2 -0
  32. data/lib/otto/design_system.rb +2 -2
  33. data/lib/otto/env_keys.rb +61 -12
  34. data/lib/otto/helpers/base.rb +2 -2
  35. data/lib/otto/helpers/request.rb +8 -3
  36. data/lib/otto/helpers/response.rb +2 -2
  37. data/lib/otto/helpers/validation.rb +2 -2
  38. data/lib/otto/helpers.rb +2 -0
  39. data/lib/otto/locale/config.rb +2 -2
  40. data/lib/otto/locale/middleware.rb +160 -0
  41. data/lib/otto/locale.rb +10 -0
  42. data/lib/otto/logging_helpers.rb +273 -0
  43. data/lib/otto/mcp/auth/token.rb +2 -2
  44. data/lib/otto/mcp/protocol.rb +2 -2
  45. data/lib/otto/mcp/rate_limiting.rb +2 -2
  46. data/lib/otto/mcp/registry.rb +2 -2
  47. data/lib/otto/mcp/route_parser.rb +2 -2
  48. data/lib/otto/mcp/schema_validation.rb +2 -2
  49. data/lib/otto/mcp/server.rb +2 -2
  50. data/lib/otto/mcp.rb +2 -0
  51. data/lib/otto/privacy/config.rb +2 -0
  52. data/lib/otto/privacy/geo_resolver.rb +199 -29
  53. data/lib/otto/privacy/ip_privacy.rb +2 -0
  54. data/lib/otto/privacy/redacted_fingerprint.rb +18 -8
  55. data/lib/otto/privacy.rb +2 -0
  56. data/lib/otto/response_handlers/auto.rb +2 -0
  57. data/lib/otto/response_handlers/base.rb +2 -0
  58. data/lib/otto/response_handlers/default.rb +2 -0
  59. data/lib/otto/response_handlers/factory.rb +2 -0
  60. data/lib/otto/response_handlers/json.rb +2 -0
  61. data/lib/otto/response_handlers/redirect.rb +2 -0
  62. data/lib/otto/response_handlers/view.rb +2 -0
  63. data/lib/otto/response_handlers.rb +2 -2
  64. data/lib/otto/route.rb +4 -4
  65. data/lib/otto/route_definition.rb +42 -15
  66. data/lib/otto/route_handlers/base.rb +2 -0
  67. data/lib/otto/route_handlers/class_method.rb +18 -25
  68. data/lib/otto/route_handlers/factory.rb +2 -2
  69. data/lib/otto/route_handlers/instance_method.rb +8 -5
  70. data/lib/otto/route_handlers/lambda.rb +8 -20
  71. data/lib/otto/route_handlers/logic_class.rb +23 -6
  72. data/lib/otto/route_handlers.rb +2 -2
  73. data/lib/otto/security/authentication/auth_failure.rb +2 -2
  74. data/lib/otto/security/authentication/auth_strategy.rb +11 -4
  75. data/lib/otto/security/authentication/route_auth_wrapper.rb +230 -78
  76. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  77. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +2 -0
  78. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  79. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  80. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  82. data/lib/otto/security/authentication.rb +2 -2
  83. data/lib/otto/security/authorization_error.rb +73 -0
  84. data/lib/otto/security/config.rb +2 -2
  85. data/lib/otto/security/configurator.rb +17 -2
  86. data/lib/otto/security/csrf.rb +2 -2
  87. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  88. data/lib/otto/security/middleware/ip_privacy_middleware.rb +31 -11
  89. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  90. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  91. data/lib/otto/security/rate_limiter.rb +2 -2
  92. data/lib/otto/security/rate_limiting.rb +2 -2
  93. data/lib/otto/security/validator.rb +2 -2
  94. data/lib/otto/security.rb +3 -0
  95. data/lib/otto/static.rb +2 -2
  96. data/lib/otto/utils.rb +27 -2
  97. data/lib/otto/version.rb +3 -3
  98. data/lib/otto.rb +174 -14
  99. data/otto.gemspec +7 -3
  100. metadata +24 -15
  101. data/benchmark_middleware_wrap.rb +0 -163
  102. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +0 -36
  103. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +0 -5
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/configuration.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative '../security/csrf'
6
6
  require_relative '../security/validator'
@@ -14,6 +14,7 @@ class Otto
14
14
  # Configuration module providing locale and application configuration methods
15
15
  module Configuration
16
16
  include Otto::Core::Freezable
17
+
17
18
  def configure_locale(opts)
18
19
  # Check if we have any locale configuration
19
20
  has_direct_options = opts[:available_locales] || opts[:default_locale]
@@ -68,9 +69,7 @@ class Otto
68
69
  @auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
69
70
  @auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]
70
71
 
71
- # Enable authentication middleware if strategies are configured
72
- return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
73
-
72
+ # No-op: authentication strategies are configured via @auth_config above
74
73
  end
75
74
 
76
75
  def configure_mcp(opts)
@@ -143,7 +142,6 @@ class Otto
143
142
  # Update existing @auth_config rather than creating a new one
144
143
  @auth_config[:auth_strategies] = strategies
145
144
  @auth_config[:default_auth_strategy] = default_strategy
146
-
147
145
  end
148
146
 
149
147
  # Freeze the application configuration to prevent runtime modifications.
@@ -157,38 +155,36 @@ class Otto
157
155
  # @return [self]
158
156
  def freeze_configuration!
159
157
  if frozen_configuration?
160
- Otto.logger.debug '[Otto::Configuration] Configuration already frozen, skipping' if Otto.debug
158
+ Otto.structured_log(:debug, 'Configuration already frozen', { status: 'skipped' }) if Otto.debug
161
159
  return self
162
160
  end
163
161
 
164
- Otto.logger.debug '[Otto::Configuration] Starting configuration freeze process' if Otto.debug
162
+ start_time = Otto::Utils.now_in_μs
165
163
 
166
164
  # Deep freeze configuration objects with memoization support
167
- Otto.logger.debug '[Otto::Configuration] Freezing security_config' if Otto.debug
168
165
  @security_config.deep_freeze! if @security_config.respond_to?(:deep_freeze!)
169
-
170
- Otto.logger.debug '[Otto::Configuration] Freezing locale_config' if Otto.debug
171
166
  @locale_config.deep_freeze! if @locale_config.respond_to?(:deep_freeze!)
172
-
173
- Otto.logger.debug '[Otto::Configuration] Freezing middleware stack' if Otto.debug
174
167
  @middleware.deep_freeze! if @middleware.respond_to?(:deep_freeze!)
175
168
 
176
169
  # Deep freeze configuration hashes (recursively freezes nested structures)
177
- Otto.logger.debug '[Otto::Configuration] Freezing auth_config hash' if Otto.debug
178
170
  deep_freeze_value(@auth_config) if @auth_config
179
-
180
- Otto.logger.debug '[Otto::Configuration] Freezing option hash' if Otto.debug
181
171
  deep_freeze_value(@option) if @option
182
172
 
183
173
  # Deep freeze route structures (prevent modification of nested hashes/arrays)
184
- Otto.logger.debug '[Otto::Configuration] Freezing route structures' if Otto.debug
185
174
  deep_freeze_value(@routes) if @routes
186
175
  deep_freeze_value(@routes_literal) if @routes_literal
187
176
  deep_freeze_value(@routes_static) if @routes_static
188
177
  deep_freeze_value(@route_definitions) if @route_definitions
189
178
 
190
179
  @configuration_frozen = true
191
- Otto.logger.info '[Otto::Configuration] Configuration freeze completed successfully'
180
+
181
+ duration = Otto::Utils.now_in_μs - start_time
182
+ frozen_objects = %w[security_config locale_config middleware auth_config option routes]
183
+ Otto.structured_log(:info, 'Freezing completed',
184
+ {
185
+ duration: duration,
186
+ frozen_objects: frozen_objects.join(','),
187
+ })
192
188
 
193
189
  self
194
190
  end
@@ -207,10 +203,9 @@ class Otto
207
203
  raise FrozenError, 'Cannot modify frozen configuration' if frozen_configuration?
208
204
  end
209
205
 
210
-
211
206
  def middleware_enabled?(middleware_class)
212
207
  # Only check the new middleware stack as the single source of truth
213
- @middleware && @middleware.includes?(middleware_class)
208
+ @middleware&.includes?(middleware_class)
214
209
  end
215
210
  end
216
211
  end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/error_handler.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'securerandom'
6
6
  require 'json'
@@ -11,10 +11,30 @@ class Otto
11
11
  # Error handling module providing secure error reporting and logging functionality
12
12
  module ErrorHandler
13
13
  def handle_error(error, env)
14
+ # Check if this is a registered expected error
15
+ if handler_config = @error_handlers[error.class.name]
16
+ return handle_expected_error(error, env, handler_config)
17
+ end
18
+
14
19
  # Log error details internally but don't expose them
15
20
  error_id = SecureRandom.hex(8)
16
- Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
17
- Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
21
+
22
+ # Base context pattern: create once, reuse for correlation
23
+ base_context = Otto::LoggingHelpers.request_context(env)
24
+
25
+ # Include handler context if available (set by route handlers)
26
+ log_context = base_context.merge(
27
+ error: error.message,
28
+ error_class: error.class.name,
29
+ error_id: error_id
30
+ )
31
+ log_context[:handler] = env['otto.handler'] if env['otto.handler']
32
+ log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
33
+
34
+ Otto.structured_log(:error, 'Unhandled error in request', log_context)
35
+
36
+ Otto::LoggingHelpers.log_backtrace(error,
37
+ base_context.merge(error_id: error_id))
18
38
 
19
39
  # Parse request for content negotiation
20
40
  begin
@@ -30,7 +50,21 @@ class Otto
30
50
  env['otto.error_id'] = error_id
31
51
  return found_route.call(env)
32
52
  rescue StandardError => e
33
- Otto.logger.error "[#{error_id}] Error in custom error handler: #{e.message}"
53
+ # When the custom error handler itself fails, generate a new error ID
54
+ # to distinguish it from the original error, but link them.
55
+ custom_handler_error_id = SecureRandom.hex(8)
56
+ base_context = Otto::LoggingHelpers.request_context(env)
57
+
58
+ Otto.structured_log(:error, 'Error in custom error handler',
59
+ base_context.merge(
60
+ error: e.message,
61
+ error_class: e.class.name,
62
+ error_id: custom_handler_error_id,
63
+ original_error_id: error_id # Link to original error
64
+ ))
65
+
66
+ Otto::LoggingHelpers.log_backtrace(e,
67
+ base_context.merge(error_id: custom_handler_error_id, original_error_id: error_id))
34
68
  end
35
69
  end
36
70
 
@@ -44,6 +78,102 @@ class Otto
44
78
 
45
79
  private
46
80
 
81
+ # Handle expected business logic errors with custom status codes and logging
82
+ #
83
+ # @param error [Exception] The expected error to handle
84
+ # @param env [Hash] Rack environment hash
85
+ # @param handler_config [Hash] Configuration from error_handlers registry
86
+ # @return [Array] Rack response tuple [status, headers, body]
87
+ def handle_expected_error(error, env, handler_config)
88
+ # Generate error ID for correlation (even for expected errors)
89
+ error_id = SecureRandom.hex(8)
90
+
91
+ # Base context pattern: create once, reuse for correlation
92
+ base_context = Otto::LoggingHelpers.request_context(env)
93
+
94
+ # Include handler context if available
95
+ log_context = base_context.merge(
96
+ error: error.message,
97
+ error_class: error.class.name,
98
+ error_id: error_id,
99
+ expected: true # Mark as expected error
100
+ )
101
+ log_context[:handler] = env['otto.handler'] if env['otto.handler']
102
+ log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
103
+
104
+ # Log at configured level (info/warn instead of error)
105
+ log_level = handler_config[:log_level] || :info
106
+ Otto.structured_log(log_level, 'Expected error in request', log_context)
107
+
108
+ # Build response body
109
+ response_body = if handler_config[:handler]
110
+ # Use custom handler block if provided
111
+ begin
112
+ req = Rack::Request.new(env)
113
+ result = handler_config[:handler].call(error, req)
114
+
115
+ # Validate that custom handler returned a Hash
116
+ unless result.is_a?(Hash)
117
+ base_context = Otto::LoggingHelpers.request_context(env)
118
+ Otto.structured_log(:warn, 'Custom error handler returned non-hash value',
119
+ base_context.merge(
120
+ error_class: error.class.name,
121
+ handler_result_class: result.class.name,
122
+ error_id: error_id
123
+ ))
124
+ result = { error: error.class.name.split('::').last, message: error.message }
125
+ end
126
+
127
+ result
128
+ rescue StandardError => e
129
+ # If custom handler fails, fall back to default
130
+ base_context = Otto::LoggingHelpers.request_context(env)
131
+ Otto.structured_log(:warn, 'Error in custom error handler',
132
+ base_context.merge(
133
+ error: e.message,
134
+ error_class: e.class.name,
135
+ original_error_class: error.class.name,
136
+ error_id: error_id
137
+ ))
138
+ { error: error.class.name.split('::').last, message: error.message }
139
+ end
140
+ else
141
+ # Default response body
142
+ { error: error.class.name.split('::').last, message: error.message }
143
+ end
144
+
145
+ # Add error_id in development mode
146
+ response_body[:error_id] = error_id if Otto.env?(:dev, :development)
147
+
148
+ # Content negotiation
149
+ accept_header = env['HTTP_ACCEPT'].to_s
150
+ status = handler_config[:status] || 500
151
+
152
+ if accept_header.include?('application/json')
153
+ body = JSON.generate(response_body)
154
+ headers = {
155
+ 'content-type' => 'application/json',
156
+ 'content-length' => body.bytesize.to_s
157
+ }.merge(@security_config.security_headers)
158
+
159
+ [status, headers, [body]]
160
+ else
161
+ # Plain text response
162
+ body = if Otto.env?(:dev, :development)
163
+ "#{response_body[:error]}: #{response_body[:message]} (ID: #{error_id})"
164
+ else
165
+ "#{response_body[:error]}: #{response_body[:message]}"
166
+ end
167
+
168
+ headers = {
169
+ 'content-type' => 'text/plain',
170
+ 'content-length' => body.bytesize.to_s
171
+ }.merge(@security_config.security_headers)
172
+
173
+ [status, headers, [body]]
174
+ end
175
+ end
176
+
47
177
  def secure_error_response(error_id)
48
178
  body = if Otto.env?(:dev, :development)
49
179
  "Server error (ID: #{error_id}). Check logs for details."
@@ -62,13 +192,13 @@ class Otto
62
192
  def json_error_response(error_id)
63
193
  error_data = if Otto.env?(:dev, :development)
64
194
  {
65
- error: 'Internal Server Error',
66
- message: 'Server error occurred. Check logs for details.',
195
+ error: 'Internal Server Error',
196
+ message: 'Server error occurred. Check logs for details.',
67
197
  error_id: error_id,
68
198
  }
69
199
  else
70
200
  {
71
- error: 'Internal Server Error',
201
+ error: 'Internal Server Error',
72
202
  message: 'An error occurred. Please try again later.',
73
203
  }
74
204
  end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/file_safety.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  module Core
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/freezable.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'set'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/middleware_stack.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative 'freezable'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/router.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative '../mcp/route_parser'
6
6
 
@@ -36,13 +36,28 @@ class Otto
36
36
  route.otto = self
37
37
  path_clean = path.gsub(%r{/$}, '')
38
38
  @route_definitions[route.definition] = route
39
- Otto.logger.debug "route: #{route.pattern}" if Otto.debug
39
+ Otto.structured_log(:debug, "Route loaded",
40
+ {
41
+ pattern: route.pattern.source,
42
+ verb: route.verb,
43
+ definition: route.definition,
44
+ type: 'pattern'
45
+ }
46
+ ) if Otto.debug
40
47
  @routes[route.verb] ||= []
41
48
  @routes[route.verb] << route
42
49
  @routes_literal[route.verb] ||= {}
43
50
  @routes_literal[route.verb][path_clean] = route
44
51
  rescue StandardError => e
45
- Otto.logger.error "Error for route #{path}: #{e.message}"
52
+ Otto.structured_log(:error, "Route load failed",
53
+ {
54
+ path: path,
55
+ verb: verb,
56
+ definition: definition,
57
+ error: e.message,
58
+ error_class: e.class.name
59
+ }
60
+ )
46
61
  Otto.logger.debug e.backtrace.join("\n") if Otto.debug
47
62
  end
48
63
  self
@@ -81,14 +96,30 @@ class Otto
81
96
  literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
82
97
 
83
98
  if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
84
- # Otto.logger.debug " request: #{path_info} (static)"
99
+ Otto.structured_log(:debug, "Route matched",
100
+ Otto::LoggingHelpers.request_context(env).merge(
101
+ type: 'static_cached',
102
+ base_path: base_path
103
+ )
104
+ )
85
105
  static_route.call(env)
86
106
  elsif literal_routes.has_key?(path_info_clean)
87
107
  route = literal_routes[path_info_clean]
88
- # Otto.logger.debug " request: #{http_verb} #{path_info} (literal route: #{route.verb} #{route.path})"
108
+ Otto.structured_log(:debug, "Route matched",
109
+ Otto::LoggingHelpers.request_context(env).merge(
110
+ type: 'literal',
111
+ handler: route.route_definition.definition,
112
+ auth_strategy: route.route_definition.auth_requirement || 'none'
113
+ )
114
+ )
89
115
  route.call(env)
90
116
  elsif static_route && http_verb == :GET && safe_file?(path_info)
91
- Otto.logger.debug " new static route: #{base_path} (#{path_info})" if Otto.debug
117
+ Otto.structured_log(:debug, "Route matched",
118
+ Otto::LoggingHelpers.request_context(env).merge(
119
+ type: 'static_new',
120
+ base_path: base_path
121
+ )
122
+ )
92
123
  routes_static[:GET][base_path] = base_path
93
124
  static_route.call(env)
94
125
  else
@@ -123,7 +154,6 @@ class Otto
123
154
  valid_routes.push(*routes[:GET]) if http_verb == :HEAD
124
155
 
125
156
  valid_routes.each do |route|
126
- # Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
127
157
  next unless (match = route.pattern.match(path_info))
128
158
 
129
159
  values = match.captures.to_a
@@ -133,13 +163,36 @@ class Otto
133
163
  values.shift
134
164
  extra_params = build_route_params(route, values)
135
165
  found_route = route
166
+
167
+ # Log successful route match
168
+ Otto.structured_log(:debug, "Route matched",
169
+ Otto::LoggingHelpers.request_context(env).merge(
170
+ pattern: route.pattern.source,
171
+ handler: route.route_definition.definition,
172
+ auth_strategy: route.route_definition.auth_requirement || 'none',
173
+ route_params: extra_params
174
+ )
175
+ )
136
176
  break
137
177
  end
138
178
 
139
179
  found_route ||= literal_routes['/404']
140
180
  if found_route
181
+ # Log 404 route usage if we fell back to it
182
+ if found_route == literal_routes['/404']
183
+ Otto.structured_log(:info, "Route not found",
184
+ Otto::LoggingHelpers.request_context(env).merge(
185
+ fallback_to: '404_route'
186
+ )
187
+ )
188
+ end
141
189
  found_route.call env, extra_params
142
190
  else
191
+ Otto.structured_log(:info, "Route not found",
192
+ Otto::LoggingHelpers.request_context(env).merge(
193
+ fallback_to: 'default_not_found'
194
+ )
195
+ )
143
196
  @not_found || Otto::Static.not_found
144
197
  end
145
198
  end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/core/uri_generator.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'uri'
6
6
 
data/lib/otto/core.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/otto/core.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'core/router'
4
6
  require_relative 'core/file_safety'
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/design_system.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  # Shared design system for Otto framework examples
data/lib/otto/env_keys.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # lib/otto/env_keys.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+ #
3
5
  # Central registry of all env['otto.*'] keys used throughout Otto framework.
4
6
  # This documentation helps prevent key conflicts and aids multi-app integration.
5
7
  #
@@ -43,19 +45,11 @@ class Otto
43
45
  # - Routes WITHOUT auth requirement: Anonymous StrategyResult
44
46
  STRATEGY_RESULT = 'otto.strategy_result'
45
47
 
46
- # Authenticated user object (convenience accessor)
47
- # Type: Hash, Custom User Object, or nil
48
- # Set by: RouteAuthWrapper (from strategy_result.user)
49
- # Used by: Controllers, RouteHandlers
50
- # Note: nil for anonymous/unauthenticated requests
51
- USER = 'otto.user'
48
+ # REMOVED: Use strategy_result.user instead
49
+ # USER = 'otto.user'
52
50
 
53
- # User-specific context (session, roles, permissions, etc.)
54
- # Type: Hash
55
- # Set by: RouteAuthWrapper (from strategy_result.user_context)
56
- # Used by: Controllers, Analytics
57
- # Note: Empty hash {} for anonymous requests
58
- USER_CONTEXT = 'otto.user_context'
51
+ # REMOVED: Use strategy_result.metadata instead
52
+ # USER_CONTEXT = 'otto.user_context'
59
53
 
60
54
  # =========================================================================
61
55
  # SECURITY & CONFIGURATION
@@ -105,6 +99,61 @@ class Otto
105
99
  # Used by: Error responses, logging, support
106
100
  ERROR_ID = 'otto.error_id'
107
101
 
102
+ # =========================================================================
103
+ # PRIVACY (IP MASKING)
104
+ # =========================================================================
105
+
106
+ # Privacy-safe masked IP address
107
+ # Type: String (e.g., '192.168.1.0')
108
+ # Set by: IPPrivacyMiddleware
109
+ # Used by: Rate limiting, analytics, logging
110
+ module Privacy
111
+ MASKED_IP = 'otto.privacy.masked_ip'
112
+
113
+ # Geo-location country code
114
+ # Type: String (ISO 3166-1 alpha-2)
115
+ # Set by: IPPrivacyMiddleware
116
+ # Used by: Analytics, localization
117
+ GEO_COUNTRY = 'otto.privacy.geo_country'
118
+
119
+ # Daily-rotating IP hash for session correlation
120
+ # Type: String (hexadecimal)
121
+ # Set by: IPPrivacyMiddleware
122
+ # Used by: Session correlation without storing IPs
123
+ HASHED_IP = 'otto.privacy.hashed_ip'
124
+
125
+ # Privacy fingerprint object
126
+ # Type: Otto::Privacy::RedactedFingerprint
127
+ # Set by: IPPrivacyMiddleware
128
+ # Used by: Full privacy context access
129
+ FINGERPRINT = 'otto.privacy.fingerprint'
130
+ end
131
+
132
+ # =========================================================================
133
+ # ORIGINAL VALUES (Privacy Disabled)
134
+ # =========================================================================
135
+
136
+ # Original client IP address (only when privacy disabled)
137
+ # Type: String
138
+ # Set by: IPPrivacyMiddleware (when privacy disabled)
139
+ # Used by: Debugging, legitimate use cases requiring real IP
140
+ # NOTE: Not available when privacy is enabled (intentional)
141
+ ORIGINAL_IP = 'otto.original_ip'
142
+
143
+ # Original User-Agent string (only when privacy disabled)
144
+ # Type: String
145
+ # Set by: IPPrivacyMiddleware (when privacy disabled)
146
+ # Used by: Bot detection, browser feature detection
147
+ # NOTE: Not available when privacy is enabled (intentional)
148
+ ORIGINAL_USER_AGENT = 'otto.original_user_agent'
149
+
150
+ # Original Referer URL (only when privacy disabled)
151
+ # Type: String
152
+ # Set by: IPPrivacyMiddleware (when privacy disabled)
153
+ # Used by: Analytics, debugging
154
+ # NOTE: Not available when privacy is enabled (intentional)
155
+ ORIGINAL_REFERER = 'otto.original_referer'
156
+
108
157
  # =========================================================================
109
158
  # MCP (MODEL CONTEXT PROTOCOL)
110
159
  # =========================================================================
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/helpers/base.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  # Base helper methods providing core functionality for Otto applications
@@ -1,4 +1,6 @@
1
1
  # lib/otto/helpers/request.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'base'
4
6
 
@@ -56,14 +58,17 @@ class Otto
56
58
  # Get anonymized user agent string
57
59
  #
58
60
  # Returns user agent with version numbers stripped for privacy.
59
- # Only available when IP privacy is enabled (default).
61
+ # When privacy is enabled (default), env['HTTP_USER_AGENT'] is already
62
+ # anonymized by IPPrivacyMiddleware, so this just returns that value.
63
+ # When privacy is disabled, returns the raw user agent.
60
64
  #
61
- # @return [String, nil] Anonymized user agent or nil
65
+ # @return [String, nil] Anonymized (or raw if privacy disabled) user agent
62
66
  # @example
63
67
  # req.anonymized_user_agent
64
68
  # # => 'Mozilla/X.X (Windows NT X.X; Win64; x64) AppleWebKit/X.X'
69
+ # @deprecated Use env['HTTP_USER_AGENT'] directly (already anonymized when privacy enabled)
65
70
  def anonymized_user_agent
66
- redacted_fingerprint&.anonymized_ua
71
+ user_agent
67
72
  end
68
73
 
69
74
  # Get masked IP address
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/helpers/response.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative 'base'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/helpers/validation.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'loofah'
6
6
  require 'facets/file'
data/lib/otto/helpers.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/otto/helpers.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'helpers/request'
4
6
  require_relative 'helpers/response'
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/locale/config.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative '../core/freezable'
6
6