otto 2.0.0.pre8 → 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.
Files changed (53) 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/claude.yml +1 -1
  5. data/.github/workflows/code-smells.yml +2 -2
  6. data/CHANGELOG.rst +73 -35
  7. data/CLAUDE.md +44 -0
  8. data/Gemfile.lock +7 -7
  9. data/README.md +20 -0
  10. data/docs/.gitignore +2 -0
  11. data/docs/modern-authentication-authorization-landscape.md +558 -0
  12. data/docs/multi-strategy-authentication-design.md +1401 -0
  13. data/lib/otto/core/error_handler.rb +104 -10
  14. data/lib/otto/core/freezable.rb +0 -2
  15. data/lib/otto/core/helper_registry.rb +135 -0
  16. data/lib/otto/core/lifecycle_hooks.rb +63 -0
  17. data/lib/otto/core/middleware_management.rb +70 -0
  18. data/lib/otto/core/middleware_stack.rb +12 -8
  19. data/lib/otto/core/router.rb +25 -31
  20. data/lib/otto/core.rb +3 -0
  21. data/lib/otto/errors.rb +92 -0
  22. data/lib/otto/helpers.rb +2 -2
  23. data/lib/otto/locale/middleware.rb +1 -1
  24. data/lib/otto/mcp/core.rb +33 -0
  25. data/lib/otto/mcp/protocol.rb +1 -1
  26. data/lib/otto/mcp/rate_limiting.rb +6 -2
  27. data/lib/otto/mcp/schema_validation.rb +2 -2
  28. data/lib/otto/mcp.rb +1 -0
  29. data/lib/otto/privacy/core.rb +82 -0
  30. data/lib/otto/privacy.rb +1 -0
  31. data/lib/otto/{helpers/request.rb → request.rb} +17 -6
  32. data/lib/otto/{helpers/response.rb → response.rb} +20 -7
  33. data/lib/otto/response_handlers/json.rb +1 -3
  34. data/lib/otto/response_handlers/view.rb +1 -1
  35. data/lib/otto/route.rb +2 -4
  36. data/lib/otto/route_handlers/base.rb +88 -5
  37. data/lib/otto/route_handlers/class_method.rb +9 -67
  38. data/lib/otto/route_handlers/instance_method.rb +10 -57
  39. data/lib/otto/route_handlers/lambda.rb +2 -2
  40. data/lib/otto/route_handlers/logic_class.rb +85 -90
  41. data/lib/otto/security/authentication/auth_strategy.rb +2 -2
  42. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
  43. data/lib/otto/security/authentication/strategy_result.rb +9 -9
  44. data/lib/otto/security/authorization_error.rb +1 -1
  45. data/lib/otto/security/config.rb +3 -3
  46. data/lib/otto/security/core.rb +167 -0
  47. data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
  48. data/lib/otto/security/middleware/validation_middleware.rb +1 -1
  49. data/lib/otto/security/rate_limiter.rb +7 -3
  50. data/lib/otto/security.rb +1 -0
  51. data/lib/otto/version.rb +1 -1
  52. data/lib/otto.rb +29 -404
  53. metadata +12 -3
@@ -1,7 +1,6 @@
1
1
  # lib/otto/route_handlers/instance_method.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
- require 'securerandom'
5
4
 
6
5
  require_relative 'base'
7
6
 
@@ -17,62 +16,16 @@ class Otto
17
16
  # Use this handler for endpoints requiring request-level control (logout,
18
17
  # session management, cookie manipulation, custom header handling).
19
18
  class InstanceMethodHandler < BaseHandler
20
- def call(env, extra_params = {})
21
- start_time = Otto::Utils.now_in_μs
22
- req = Rack::Request.new(env)
23
- res = Rack::Response.new
24
-
25
- begin
26
- # Apply the same extensions and processing as original Route#call
27
- setup_request_response(req, res, env, extra_params)
28
-
29
- # Create instance and call method (existing Otto behavior)
30
- instance = target_class.new(req, res)
31
- result = instance.send(route_definition.method_name)
32
-
33
- # Only handle response if response_type is not default
34
- if route_definition.response_type != 'default'
35
- handle_response(result, res, {
36
- instance: instance,
37
- request: req,
38
- })
39
- end
40
- rescue StandardError => e
41
- # Check if we're being called through Otto's integrated context (vs direct handler testing)
42
- # In integrated context, let Otto's centralized error handler manage the response
43
- # In direct testing context, handle errors locally for unit testing
44
- if otto_instance
45
- # Store handler context in env for centralized error handler
46
- handler_name = "#{target_class}##{route_definition.method_name}"
47
- env['otto.handler'] = handler_name
48
- env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
49
-
50
- raise e # Re-raise to let Otto's centralized error handler manage the response
51
- else
52
- # Direct handler testing context - handle errors locally with security improvements
53
- error_id = SecureRandom.hex(8)
54
- Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
55
- Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
56
-
57
- res.status = 500
58
- res.headers['content-type'] = 'text/plain'
59
-
60
- if Otto.env?(:dev, :development)
61
- res.write "Server error (ID: #{error_id}). Check logs for details."
62
- else
63
- res.write 'An error occurred. Please try again later.'
64
- end
65
-
66
- # Add security headers if available
67
- if otto_instance.respond_to?(:security_config) && otto_instance.security_config
68
- otto_instance.security_config.security_headers.each do |header, value|
69
- res.headers[header] = value
70
- end
71
- end
72
- end
73
- end
74
-
75
- finalize_response(res)
19
+ protected
20
+
21
+ # Invoke the instance method on the target class
22
+ # @param req [Rack::Request] Request object
23
+ # @param res [Rack::Response] Response object
24
+ # @return [Array] [result, context] for handle_response
25
+ def invoke_target(req, res)
26
+ instance = target_class.new(req, res)
27
+ result = instance.send(route_definition.method_name)
28
+ [result, { instance: instance, request: req }]
76
29
  end
77
30
  end
78
31
  end
@@ -11,8 +11,8 @@ class Otto
11
11
  class LambdaHandler < BaseHandler
12
12
  def call(env, extra_params = {})
13
13
  start_time = Otto::Utils.now_in_μs
14
- req = Rack::Request.new(env)
15
- res = Rack::Response.new
14
+ req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
15
+ res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
16
16
 
17
17
  begin
18
18
  # Security: Lambda handlers require pre-configured procs from Otto instance
@@ -1,8 +1,6 @@
1
1
  # lib/otto/route_handlers/logic_class.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
- require 'json'
5
- require 'securerandom'
6
4
 
7
5
  require_relative 'base'
8
6
 
@@ -20,98 +18,95 @@ class Otto
20
18
  # For endpoints requiring direct request access (sessions, cookies, headers,
21
19
  # or logout flows), use controller handlers (Controller#action or Controller.action).
22
20
  class LogicClassHandler < BaseHandler
23
- def call(env, extra_params = {})
24
- start_time = Otto::Utils.now_in_μs
25
- req = Rack::Request.new(env)
26
- res = Rack::Response.new
21
+ protected
27
22
 
23
+ # Invoke Logic class with constrained signature
24
+ # @param req [Rack::Request] Request object
25
+ # @param res [Rack::Response] Response object
26
+ # @return [Array] [result, context] for handle_response
27
+ def invoke_target(req, _res)
28
+ env = req.env
29
+
30
+ # Get strategy result (guaranteed to exist from RouteAuthWrapper)
31
+ strategy_result = env['otto.strategy_result']
32
+
33
+ # Extract params including JSON body parsing
34
+ logic_params = extract_logic_params(req, env)
35
+
36
+ # Get locale
37
+ locale = env['otto.locale'] || 'en'
38
+
39
+ # Instantiate Logic class
40
+ logic = target_class.new(strategy_result, logic_params, locale)
41
+
42
+ # Execute standard Logic class lifecycle
43
+ logic.raise_concerns if logic.respond_to?(:raise_concerns)
44
+
45
+ result = if logic.respond_to?(:process)
46
+ logic.process
47
+ else
48
+ logic.call || logic
49
+ end
50
+
51
+ context = {
52
+ logic_instance: logic,
53
+ request: req,
54
+ status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
55
+ }
56
+
57
+ [result, context]
58
+ end
59
+
60
+ # Extract logic parameters including JSON body parsing
61
+ # @param req [Rack::Request] Request object
62
+ # @param env [Hash] Rack environment
63
+ # @return [Hash] Parameters for Logic class
64
+ def extract_logic_params(req, env)
65
+ # req.params already has extra_params merged and indifferent_params applied
66
+ # by setup_request_response in BaseHandler
67
+ logic_params = req.params.dup
68
+
69
+ # Handle JSON request bodies
70
+ if req.content_type&.include?('application/json') && req.body.size.positive?
71
+ logic_params = parse_json_body(req, env, logic_params)
72
+ end
73
+
74
+ logic_params
75
+ end
76
+
77
+ # Parse JSON request body with error handling
78
+ # @param req [Rack::Request] Request object
79
+ # @param env [Hash] Rack environment
80
+ # @param logic_params [Hash] Current parameters
81
+ # @return [Hash] Parameters with JSON merged (or original if parsing fails)
82
+ def parse_json_body(req, env, logic_params)
28
83
  begin
29
- # Get strategy result (guaranteed to exist from RouteAuthWrapper)
30
- strategy_result = env['otto.strategy_result']
31
-
32
- # Initialize Logic class with new signature: context, params, locale
33
- logic_params = req.params.merge(extra_params)
34
-
35
- # Handle JSON request bodies
36
- if req.content_type&.include?('application/json') && req.body.size.positive?
37
- begin
38
- req.body.rewind
39
- json_data = JSON.parse(req.body.read)
40
- logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
41
- rescue JSON::ParserError => e
42
- # Base context pattern: create once, reuse for correlation
43
- base_context = Otto::LoggingHelpers.request_context(env)
44
-
45
- Otto.structured_log(:error, "JSON parsing error",
46
- base_context.merge(
47
- handler: "#{target_class}#call",
48
- error: e.message,
49
- error_class: e.class.name,
50
- duration: Otto::Utils.now_in_μs - start_time
51
- )
52
- )
53
-
54
- Otto::LoggingHelpers.log_backtrace(e,
55
- base_context.merge(handler: "#{target_class}#call")
56
- )
57
- end
58
- end
59
-
60
- locale = env['otto.locale'] || 'en'
61
-
62
- logic = target_class.new(strategy_result, logic_params, locale)
63
-
64
- # Execute standard Logic class lifecycle
65
- logic.raise_concerns if logic.respond_to?(:raise_concerns)
66
-
67
- result = if logic.respond_to?(:process)
68
- logic.process
69
- else
70
- logic.call || logic
71
- end
72
-
73
- # Handle response with Logic instance context
74
- handle_response(result, res, {
75
- logic_instance: logic,
76
- request: req,
77
- status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
78
- })
79
- rescue StandardError => e
80
- # Check if we're being called through Otto's integrated context (vs direct handler testing)
81
- # In integrated context, let Otto's centralized error handler manage the response
82
- # In direct testing context, handle errors locally for unit testing
83
- if otto_instance
84
- # Store handler context in env for centralized error handler
85
- handler_name = "#{target_class}#call"
86
- env['otto.handler'] = handler_name
87
- env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
88
-
89
- raise e # Re-raise to let Otto's centralized error handler manage the response
90
- else
91
- # Direct handler testing context - handle errors locally with security improvements
92
- error_id = SecureRandom.hex(8)
93
- Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
94
- Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
95
-
96
- res.status = 500
97
- res.headers['content-type'] = 'text/plain'
98
-
99
- if Otto.env?(:dev, :development)
100
- res.write "Server error (ID: #{error_id}). Check logs for details."
101
- else
102
- res.write 'An error occurred. Please try again later.'
103
- end
104
-
105
- # Add security headers if available
106
- if otto_instance.respond_to?(:security_config) && otto_instance.security_config
107
- otto_instance.security_config.security_headers.each do |header, value|
108
- res.headers[header] = value
109
- end
110
- end
111
- end
84
+ req.body.rewind
85
+ json_data = JSON.parse(req.body.read)
86
+ logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
87
+ rescue JSON::ParserError => e
88
+ # Base context pattern: create once, reuse for correlation
89
+ log_context = Otto::LoggingHelpers.request_context(env)
90
+
91
+ Otto.structured_log(:error, 'JSON parsing error',
92
+ log_context.merge(
93
+ handler: handler_name,
94
+ error: e.message,
95
+ error_class: e.class.name,
96
+ duration: Otto::Utils.now_in_μs - @start_time
97
+ ))
98
+
99
+ Otto::LoggingHelpers.log_backtrace(e,
100
+ log_context.merge(handler: handler_name))
112
101
  end
113
102
 
114
- res.finish
103
+ logic_params
104
+ end
105
+
106
+ # Format handler name for Logic routes
107
+ # @return [String] Handler name in format "ClassName#call"
108
+ def handler_name
109
+ "#{target_class.name}#call"
115
110
  end
116
111
  end
117
112
  end
@@ -1,7 +1,7 @@
1
1
  # lib/otto/security/authentication/auth_strategy.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
- #
4
+
5
5
  # Base class for all authentication strategies in Otto framework
6
6
  # Provides pluggable authentication patterns that can be customized per application
7
7
 
@@ -33,7 +33,7 @@ class Otto
33
33
  user: user,
34
34
  auth_method: auth_method || self.class.name.split('::').last,
35
35
  metadata: metadata,
36
- strategy_name: nil # Will be set by RouteAuthWrapper
36
+ strategy_name: nil # Will be set by RouteAuthWrapper
37
37
  )
38
38
  end
39
39
 
@@ -21,7 +21,7 @@ class Otto
21
21
  api_key = env["HTTP_#{@header_name.upcase.tr('-', '_')}"]
22
22
 
23
23
  if api_key.nil?
24
- request = Rack::Request.new(env)
24
+ request = Otto::Request.new(env)
25
25
  api_key = request.params[@param_name]
26
26
  end
27
27
 
@@ -320,16 +320,16 @@ class Otto
320
320
  # @return [Hash] Hash representation of the context
321
321
  def to_h
322
322
  {
323
- session: session,
324
- user: user,
325
- auth_method: auth_method,
326
- metadata: metadata,
327
- authenticated: authenticated?,
323
+ session: session,
324
+ user: user,
325
+ auth_method: auth_method,
326
+ metadata: metadata,
327
+ authenticated: authenticated?,
328
328
  auth_attempt_succeeded: auth_attempt_succeeded?,
329
- user_id: user_id,
330
- user_name: user_name,
331
- roles: roles,
332
- permissions: permissions
329
+ user_id: user_id,
330
+ user_name: user_name,
331
+ roles: roles,
332
+ permissions: permissions,
333
333
  }
334
334
  end
335
335
  end
@@ -40,7 +40,7 @@ class Otto
40
40
  # end
41
41
  # end
42
42
  #
43
- class AuthorizationError < StandardError
43
+ class AuthorizationError < Otto::ForbiddenError
44
44
  # Optional additional context for logging/debugging
45
45
  attr_reader :resource, :action, :user_id
46
46
 
@@ -436,12 +436,12 @@ class Otto
436
436
  end
437
437
 
438
438
  # Raised when a request exceeds the configured size limit
439
- class RequestTooLargeError < StandardError; end
439
+ class RequestTooLargeError < Otto::PayloadTooLargeError; end
440
440
 
441
441
  # Raised when CSRF token validation fails
442
- class CSRFError < StandardError; end
442
+ class CSRFError < Otto::ForbiddenError; end
443
443
 
444
444
  # Raised when input validation fails (XSS, SQL injection, etc.)
445
- class ValidationError < StandardError; end
445
+ class ValidationError < Otto::BadRequestError; end
446
446
  end
447
447
  end
@@ -0,0 +1,167 @@
1
+ # lib/otto/security/core.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Security
7
+ # Core security configuration methods included in the Otto class.
8
+ # Provides the public API for enabling and configuring security features.
9
+ module Core
10
+ # Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
11
+ # This will automatically add CSRF tokens to HTML forms and validate
12
+ # them on unsafe HTTP methods.
13
+ #
14
+ # @example
15
+ # otto.enable_csrf_protection!
16
+ def enable_csrf_protection!
17
+ ensure_not_frozen!
18
+ return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
19
+
20
+ @security_config.enable_csrf_protection!
21
+ use Otto::Security::Middleware::CSRFMiddleware
22
+ end
23
+
24
+ # Enable request validation including input sanitization, size limits,
25
+ # and protection against XSS and SQL injection attacks.
26
+ #
27
+ # @example
28
+ # otto.enable_request_validation!
29
+ def enable_request_validation!
30
+ ensure_not_frozen!
31
+ return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
32
+
33
+ @security_config.input_validation = true
34
+ use Otto::Security::Middleware::ValidationMiddleware
35
+ end
36
+
37
+ # Enable rate limiting to protect against abuse and DDoS attacks.
38
+ # This will automatically add rate limiting rules based on client IP.
39
+ #
40
+ # @param options [Hash] Rate limiting configuration options
41
+ # @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
42
+ # @option options [Hash] :custom_rules Custom rate limiting rules
43
+ # @example
44
+ # otto.enable_rate_limiting!(requests_per_minute: 50)
45
+ def enable_rate_limiting!(options = {})
46
+ ensure_not_frozen!
47
+ return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
48
+
49
+ @security.configure_rate_limiting(options)
50
+ use Otto::Security::Middleware::RateLimitMiddleware
51
+ end
52
+
53
+ # Add a custom rate limiting rule.
54
+ #
55
+ # @param name [String, Symbol] Rule name
56
+ # @param options [Hash] Rule configuration
57
+ # @option options [Integer] :limit Maximum requests
58
+ # @option options [Integer] :period Time period in seconds (default: 60)
59
+ # @option options [Proc] :condition Optional condition proc that receives request
60
+ # @example
61
+ # otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
62
+ def add_rate_limit_rule(name, options)
63
+ ensure_not_frozen!
64
+ @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
65
+ end
66
+
67
+ # Add a trusted proxy server for accurate client IP detection.
68
+ # Only requests from trusted proxies will have their forwarded headers honored.
69
+ #
70
+ # @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
71
+ # @example
72
+ # otto.add_trusted_proxy('10.0.0.0/8')
73
+ # otto.add_trusted_proxy(/^172\.16\./)
74
+ def add_trusted_proxy(proxy)
75
+ ensure_not_frozen!
76
+ @security_config.add_trusted_proxy(proxy)
77
+ end
78
+
79
+ # Set custom security headers that will be added to all responses.
80
+ # These merge with the default security headers.
81
+ #
82
+ # @param headers [Hash] Hash of header name => value pairs
83
+ # @example
84
+ # otto.set_security_headers({
85
+ # 'content-security-policy' => "default-src 'self'",
86
+ # 'strict-transport-security' => 'max-age=31536000'
87
+ # })
88
+ def set_security_headers(headers)
89
+ ensure_not_frozen!
90
+ @security_config.security_headers.merge!(headers)
91
+ end
92
+
93
+ # Enable HTTP Strict Transport Security (HSTS) header.
94
+ # WARNING: This can make your domain inaccessible if HTTPS is not properly
95
+ # configured. Only enable this when you're certain HTTPS is working correctly.
96
+ #
97
+ # @param max_age [Integer] Maximum age in seconds (default: 1 year)
98
+ # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
99
+ # @example
100
+ # otto.enable_hsts!(max_age: 86400, include_subdomains: false)
101
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
102
+ ensure_not_frozen!
103
+ @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
104
+ end
105
+
106
+ # Enable Content Security Policy (CSP) header to prevent XSS attacks.
107
+ # The default policy only allows resources from the same origin.
108
+ #
109
+ # @param policy [String] CSP policy string (default: "default-src 'self'")
110
+ # @example
111
+ # otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
112
+ def enable_csp!(policy = "default-src 'self'")
113
+ ensure_not_frozen!
114
+ @security_config.enable_csp!(policy)
115
+ end
116
+
117
+ # Enable X-Frame-Options header to prevent clickjacking attacks.
118
+ #
119
+ # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
120
+ # @example
121
+ # otto.enable_frame_protection!('DENY')
122
+ def enable_frame_protection!(option = 'SAMEORIGIN')
123
+ ensure_not_frozen!
124
+ @security_config.enable_frame_protection!(option)
125
+ end
126
+
127
+ # Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
128
+ # This enables the res.send_csp_headers response helper method.
129
+ #
130
+ # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
131
+ # @example
132
+ # otto.enable_csp_with_nonce!(debug: true)
133
+ def enable_csp_with_nonce!(debug: false)
134
+ ensure_not_frozen!
135
+ @security_config.enable_csp_with_nonce!(debug: debug)
136
+ end
137
+
138
+ # Add an authentication strategy with a registered name
139
+ #
140
+ # This is the primary public API for registering authentication strategies.
141
+ # The name you provide here will be available as `strategy_result.strategy_name`
142
+ # in your application code, making it easy to identify which strategy authenticated
143
+ # the current request.
144
+ #
145
+ # Also available via Otto::Security::Configurator for consolidated security config.
146
+ #
147
+ # @param name [String, Symbol] Strategy name (e.g., 'session', 'api_key', 'jwt')
148
+ # @param strategy [AuthStrategy] Strategy instance
149
+ # @example
150
+ # otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
151
+ # otto.add_auth_strategy('api_key', APIKeyStrategy.new)
152
+ # @raise [ArgumentError] if strategy name already registered
153
+ def add_auth_strategy(name, strategy)
154
+ ensure_not_frozen!
155
+ # Ensure auth_config is initialized (handles edge case where it might be nil)
156
+ @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
157
+
158
+ # Strict mode: Detect strategy name collisions
159
+ if @auth_config[:auth_strategies].key?(name)
160
+ raise ArgumentError, "Authentication strategy '#{name}' is already registered"
161
+ end
162
+
163
+ @auth_config[:auth_strategies][name] = strategy
164
+ end
165
+ end
166
+ end
167
+ end
@@ -19,7 +19,7 @@ class Otto
19
19
  def call(env)
20
20
  return @app.call(env) unless @config.csrf_enabled?
21
21
 
22
- request = Rack::Request.new(env)
22
+ request = Otto::Request.new(env)
23
23
 
24
24
  # Skip CSRF protection for safe methods
25
25
  if safe_method?(request.request_method)
@@ -32,7 +32,7 @@ class Otto
32
32
  def call(env)
33
33
  return @app.call(env) unless @config.input_validation
34
34
 
35
- request = Rack::Request.new(env)
35
+ request = Otto::Request.new(env)
36
36
 
37
37
  begin
38
38
  # Validate request size
@@ -55,9 +55,13 @@ class Otto
55
55
  'retry-after' => (match_data[:period] - (now % match_data[:period])).to_s,
56
56
  }
57
57
 
58
- # Check if request expects JSON
59
- accept_header = request.env['HTTP_ACCEPT'].to_s
60
- if accept_header.include?('application/json')
58
+ # Content negotiation for rate limit response
59
+ # Route's response_type takes precedence over Accept header
60
+ route_def = request.env['otto.route_definition']
61
+ wants_json = (route_def&.response_type == 'json') ||
62
+ request.env['HTTP_ACCEPT'].to_s.include?('application/json')
63
+
64
+ if wants_json
61
65
  error_response = {
62
66
  error: 'Rate limit exceeded',
63
67
  message: 'Too many requests',
data/lib/otto/security.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'security/core'
5
6
  require_relative 'security/authentication/strategy_result'
6
7
  require_relative 'security/authorization_error'
7
8
  require_relative 'security/config'
data/lib/otto/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
- VERSION = '2.0.0.pre8'
6
+ VERSION = '2.0.0.pre10'
7
7
  end