otto 2.3.1 → 2.4.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +1 -1
  3. data/.github/workflows/ci.yml +7 -1
  4. data/.github/workflows/claude-code-review.yml +32 -9
  5. data/.github/workflows/claude.yml +7 -5
  6. data/.github/workflows/code-smells.yml +2 -2
  7. data/.github/workflows/release-gem.yml +12 -2
  8. data/.github/workflows/ruby-lint.yml +66 -0
  9. data/.github/workflows/yardoc.yml +117 -0
  10. data/.yardopts +15 -0
  11. data/CHANGELOG.rst +59 -0
  12. data/Gemfile +4 -2
  13. data/Gemfile.lock +23 -17
  14. data/README.md +96 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/reverse-proxy-network-services.md +358 -0
  17. data/examples/caddy_tls_demo/README.md +100 -0
  18. data/examples/caddy_tls_demo/app.rb +41 -0
  19. data/examples/caddy_tls_demo/config.ru +31 -0
  20. data/examples/caddy_tls_demo/routes +9 -0
  21. data/examples/caddy_tls_demo/standalone.ru +38 -0
  22. data/lib/otto/caddy_tls/core.rb +74 -0
  23. data/lib/otto/caddy_tls/localhost_guard.rb +158 -0
  24. data/lib/otto/caddy_tls/server.rb +149 -0
  25. data/lib/otto/caddy_tls.rb +7 -0
  26. data/lib/otto/core/middleware_management.rb +7 -7
  27. data/lib/otto/core/middleware_stack.rb +39 -5
  28. data/lib/otto/core/router.rb +4 -8
  29. data/lib/otto/security/config.rb +227 -2
  30. data/lib/otto/security/configurator.rb +38 -0
  31. data/lib/otto/security/core.rb +62 -0
  32. data/lib/otto/security/csp/parser.rb +120 -0
  33. data/lib/otto/security/csp/report.rb +147 -0
  34. data/lib/otto/security/csp/report_middleware.rb +120 -0
  35. data/lib/otto/security/csp.rb +19 -0
  36. data/lib/otto/security/middleware/ip_privacy_middleware.rb +72 -7
  37. data/lib/otto/security.rb +1 -0
  38. data/lib/otto/utils.rb +36 -0
  39. data/lib/otto/version.rb +1 -1
  40. data/lib/otto.rb +26 -3
  41. metadata +23 -3
@@ -0,0 +1,120 @@
1
+ # lib/otto/security/csp/report_middleware.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'parser'
6
+
7
+ class Otto
8
+ module Security
9
+ module CSP
10
+ # Rack middleware that receives browser-posted Content-Security-Policy
11
+ # violation reports and dispatches them to an application callback.
12
+ #
13
+ # This is the receiving half of Otto's CSP support; the emitting half is
14
+ # {Otto::Security::Config#generate_nonce_csp} / {Otto::Response#send_csp_headers}
15
+ # (and the static {Otto::Security::Config#enable_csp!}). When a report URI
16
+ # is configured, the emitted policy carries a `report-uri` directive
17
+ # pointing here.
18
+ #
19
+ # Behavior (all mandatory for a public, unauthenticated receiver):
20
+ #
21
+ # - INERT unless {Otto::Security::Config#csp_report_uri} is set. When it is
22
+ # not configured the middleware is a transparent pass-through.
23
+ # - Only intercepts a POST whose path matches the configured report URI.
24
+ # Everything else (other paths, other methods) passes through untouched.
25
+ # - Short-circuits BEFORE inner middleware, so CSRF, auth, and rate
26
+ # limiting never see the request. This is why browsers can POST reports
27
+ # with no CSRF token: the report never reaches the CSRF middleware.
28
+ # {Otto::Security::Core#enable_csp_reporting!} pins this middleware
29
+ # OUTERMOST (via the :outermost stack position), so the guarantee holds
30
+ # regardless of the order security features are enabled in. The flip side
31
+ # is that reports also bypass rate limiting — see the DoS note on
32
+ # {Otto::Security::Core#enable_csp_reporting!}; keep callbacks cheap.
33
+ # - Enforces a hard {MAX_BODY_BYTES} body cap. Oversized bodies are
34
+ # detected with a `cap + 1` read and skipped WITHOUT parsing, so a
35
+ # hostile client cannot force large allocations against a public endpoint.
36
+ # - Parses both wire formats via {Otto::Security::CSP::Parser} and invokes
37
+ # the registered callback once per normalized report.
38
+ # - NEVER raises to the client and always responds `204 No Content`
39
+ # (browsers ignore the body). A throwing callback is isolated by
40
+ # {Otto::Security::Config#dispatch_csp_violation}.
41
+ class ReportMiddleware
42
+ # Hard cap on the request body we are willing to read/parse. Browsers
43
+ # send small JSON documents; anything larger is abuse and is dropped.
44
+ MAX_BODY_BYTES = 64 * 1024 # 64 KiB
45
+
46
+ def initialize(app, config = nil)
47
+ @app = app
48
+ @config = config || Otto::Security::Config.new
49
+ end
50
+
51
+ def call(env)
52
+ return @app.call(env) unless report_request?(env)
53
+
54
+ receive_report(env)
55
+ end
56
+
57
+ private
58
+
59
+ # Handle a report POST and always answer 204. A report receiver must
60
+ # never surface an error to the browser, so parse/dispatch failures are
61
+ # contained HERE — deliberately NOT around the #call pass-through, which
62
+ # would swallow unrelated downstream errors (turning every failing
63
+ # request into a silent 204, since this middleware runs outermost).
64
+ def receive_report(env)
65
+ handle_report(env)
66
+ # A fresh header hash + body per call (never a shared/frozen literal)
67
+ # so a downstream server that mutates the response tuple is safe.
68
+ [204, {}, []]
69
+ rescue StandardError => e
70
+ Otto.logger.error("[Otto::CSP] report handling failed: #{e.class}: #{e.message}")
71
+ [204, {}, []]
72
+ end
73
+
74
+ # True only when reporting is configured AND this is a POST to the
75
+ # configured report path.
76
+ #
77
+ # @param env [Hash]
78
+ # @return [Boolean]
79
+ def report_request?(env)
80
+ report_uri = @config.csp_report_uri
81
+ return false if report_uri.nil? || report_uri.empty?
82
+ return false unless env['REQUEST_METHOD'] == 'POST'
83
+
84
+ env['PATH_INFO'] == report_uri
85
+ end
86
+
87
+ # Read (capped), parse, and dispatch. Never raises; parse/dispatch
88
+ # failures are contained so the caller can still return 204.
89
+ #
90
+ # @param env [Hash]
91
+ # @return [void]
92
+ def handle_report(env)
93
+ body = read_capped_body(env)
94
+ return if body.nil?
95
+
96
+ reports = Otto::Security::CSP::Parser.parse(body, env['CONTENT_TYPE'])
97
+ reports.each { |report| @config.dispatch_csp_violation(report) }
98
+ end
99
+
100
+ # Read at most MAX_BODY_BYTES + 1 bytes so an oversized body is detected
101
+ # without ever materializing more than the cap. Returns nil when there is
102
+ # no readable body or the body exceeds the cap (skip without parsing).
103
+ #
104
+ # @param env [Hash]
105
+ # @return [String, nil]
106
+ def read_capped_body(env)
107
+ input = env['rack.input']
108
+ return nil if input.nil?
109
+
110
+ chunk = input.read(MAX_BODY_BYTES + 1)
111
+ input.rewind if input.respond_to?(:rewind)
112
+ return nil if chunk.nil? || chunk.empty?
113
+ return nil if chunk.bytesize > MAX_BODY_BYTES # oversized: drop unparsed
114
+
115
+ chunk
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,19 @@
1
+ # lib/otto/security/csp.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ #
6
+ # Index file for Content-Security-Policy violation reporting components.
7
+ #
8
+ # Otto emits CSP headers via Otto::Security::Config (static #enable_csp! and
9
+ # nonce-based #generate_nonce_csp / Otto::Response#send_csp_headers). This
10
+ # module adds the receiving half: a turnkey violation-report endpoint plus a
11
+ # callback API, so an application can collect CSP reports with a few lines of
12
+ # config instead of hand-rolling a parser, size cap, and CSRF exemption.
13
+ #
14
+ # See Otto::Security::Core#enable_csp_reporting! for the primary entry point and
15
+ # delano/otto#174 for the design.
16
+
17
+ require_relative 'csp/report'
18
+ require_relative 'csp/parser'
19
+ require_relative 'csp/report_middleware'
@@ -85,6 +85,27 @@ class Otto
85
85
 
86
86
  Otto.logger.debug "[IPPrivacyMiddleware] Resolved client IP: #{client_ip}" if Otto.debug
87
87
 
88
+ # No resolvable client IP (REMOTE_ADDR absent or blank, and no trusted
89
+ # forwarded value). There is nothing to mask, and masking would derive
90
+ # a nil masked IP (IPPrivacy.mask_ip returns nil for nil/empty input).
91
+ # Writing that nil back to REMOTE_ADDR / forwarded headers would leave
92
+ # present-but-nil CGI keys, which violate the Rack SPEC and trip
93
+ # Rack::Lint — the same class of bug as the User-Agent/Referer case
94
+ # below (issue #167). Skip the IP-masking work, leaving REMOTE_ADDR
95
+ # untouched (an absent key stays absent; an empty string stays an
96
+ # empty string).
97
+ #
98
+ # The User-Agent/Referer redaction, however, is independent of the
99
+ # client IP, and this middleware's contract is to ALWAYS clear the
100
+ # original sensitive data. So still scrub those headers before
101
+ # bailing — a request with no resolvable IP must not leak an
102
+ # un-anonymized User-Agent or Referer.
103
+ if client_ip.to_s.empty?
104
+ Otto.logger.debug '[IPPrivacyMiddleware] No resolvable client IP; skipping IP masking' if Otto.debug
105
+ scrub_sensitive_headers(env, Otto::Privacy::RedactedFingerprint.new(env, @config))
106
+ return
107
+ end
108
+
88
109
  # Skip masking for private/localhost IPs unless explicitly configured to mask them
89
110
  # This provides better DX for development while still protecting public IPs
90
111
  unless @config.mask_private_ips
@@ -122,13 +143,10 @@ class Otto
122
143
  # Privacy-safe: holds the masked value, never the original public IP.
123
144
  env['otto.client_ip'] = fingerprint.masked_ip
124
145
 
125
- # Replace User-Agent with anonymized version (consistent with IP masking)
126
- # CRITICAL: Always replace, even if nil, to clear original sensitive data
127
- env['HTTP_USER_AGENT'] = fingerprint.anonymized_ua
128
-
129
- # Replace Referer with anonymized version (query params stripped)
130
- # CRITICAL: Always replace, even if nil, to clear original sensitive data
131
- env['HTTP_REFERER'] = fingerprint.referer
146
+ # Replace User-Agent / Referer with anonymized versions (consistent
147
+ # with IP masking). See scrub_sensitive_headers also reached by the
148
+ # no-resolvable-IP path so these headers are always cleared.
149
+ scrub_sensitive_headers(env, fingerprint)
132
150
 
133
151
  # Mask X-Forwarded-For headers to prevent leakage
134
152
  # Replace with masked IP so proxy resolution logic finds the masked IP
@@ -141,6 +159,45 @@ class Otto
141
159
  end
142
160
 
143
161
 
162
+ # Set or clear a Rack env header in a SPEC-compliant way.
163
+ #
164
+ # CGI-style keys (those without a period) must hold String values per
165
+ # the Rack SPEC; a present-but-nil value trips Rack::Lint. So when the
166
+ # anonymized replacement is nil, delete the key entirely instead of
167
+ # assigning nil — semantically identical to "cleared" for downstream
168
+ # readers, and SPEC-compliant.
169
+ #
170
+ # @param env [Hash] Rack environment
171
+ # @param key [String] Env key to set or delete
172
+ # @param value [String, nil] Replacement value, or nil to clear the key
173
+ def replace_or_delete(env, key, value)
174
+ if value.nil?
175
+ env.delete(key)
176
+ else
177
+ env[key] = value
178
+ end
179
+ end
180
+
181
+ # Redact the request's sensitive non-IP headers in place.
182
+ #
183
+ # User-Agent and Referer carry identifying information independent of
184
+ # the client IP, so they are scrubbed on every privacy-enabled request
185
+ # — including ones with no resolvable IP, where IP masking is skipped.
186
+ # Each header is replaced with the fingerprint's anonymized value, or
187
+ # DELETED when that value is nil (no/empty header): CGI-style keys must
188
+ # hold String values per the Rack SPEC, so a present-but-nil
189
+ # HTTP_USER_AGENT/HTTP_REFERER would trip Rack::Lint (issue #167).
190
+ # Deleting is also marginally more private — an absent header is
191
+ # indistinguishable from one that was never sent.
192
+ #
193
+ # @param env [Hash] Rack environment
194
+ # @param fingerprint [Otto::Privacy::RedactedFingerprint] source of the
195
+ # anonymized header values
196
+ def scrub_sensitive_headers(env, fingerprint)
197
+ replace_or_delete(env, 'HTTP_USER_AGENT', fingerprint.anonymized_ua)
198
+ replace_or_delete(env, 'HTTP_REFERER', fingerprint.referer)
199
+ end
200
+
144
201
  # Resolve the actual client IP address from the request.
145
202
  #
146
203
  # Delegates to the shared Otto::Utils.resolve_client_ip so the
@@ -162,6 +219,14 @@ class Otto
162
219
  # @param env [Hash] Rack environment
163
220
  # @param masked_ip [String] The masked IP to use as replacement
164
221
  def mask_forwarded_headers(env, masked_ip)
222
+ # Defensive: never write a nil replacement into these CGI-style headers
223
+ # (the Rack SPEC requires String values; a nil trips Rack::Lint — see
224
+ # issue #167). apply_privacy's early "no client IP" guard already
225
+ # guarantees a non-nil masked_ip here, but keep this method
226
+ # self-contained so a future caller change can't reintroduce a
227
+ # present-but-nil HTTP_X_FORWARDED_FOR.
228
+ return if masked_ip.nil?
229
+
165
230
  # Replace X-Forwarded-For with masked IP
166
231
  # This prevents Rack::Request#ip from finding the real IP
167
232
  env['HTTP_X_FORWARDED_FOR'] = masked_ip if env['HTTP_X_FORWARDED_FOR']
data/lib/otto/security.rb CHANGED
@@ -12,3 +12,4 @@ require_relative 'security/middleware/csrf_middleware'
12
12
  require_relative 'security/middleware/validation_middleware'
13
13
  require_relative 'security/middleware/rate_limit_middleware'
14
14
  require_relative 'security/middleware/ip_privacy_middleware'
15
+ require_relative 'security/csp'
data/lib/otto/utils.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'ipaddr'
6
+ require 'rack/utils'
6
7
 
7
8
  class Otto
8
9
  # Utility methods for common operations and helpers
@@ -57,6 +58,41 @@ class Otto
57
58
  !value.to_s.empty? && %w[true yes 1].include?(value.to_s.downcase)
58
59
  end
59
60
 
61
+ # Canonical path normalization for literal route matching: URL-unescape,
62
+ # scrub invalid/undefined bytes, and strip a single trailing slash.
63
+ #
64
+ # This is the SINGLE SOURCE OF TRUTH shared by the router
65
+ # (Otto::Core::Router#handle_request, which compares the result against its
66
+ # literal-route table) and Otto::CaddyTLS::LocalhostGuard (which compares it
67
+ # against the guarded endpoint). The two MUST normalize identically: if a
68
+ # crafted path — a trailing slash, a percent-encoded byte, an invalid UTF-8
69
+ # byte — normalized differently in the guard than in the router, the router
70
+ # could dispatch a request the guard let through, bypassing the loopback
71
+ # check. One implementation makes that drift impossible.
72
+ #
73
+ # Mirrors the empty-path handling and :replace scrubbing the router applies.
74
+ # Robust to invalid input: Rack::Utils.unescape raises ArgumentError on an
75
+ # already-invalid byte sequence (a raw \xFF in the path), so that is caught
76
+ # and the raw string is scrubbed instead — a percent-encoded invalid byte
77
+ # (%FF) decodes to the same invalid byte and is scrubbed identically, so the
78
+ # two crafted forms normalize alike. The method itself does not raise.
79
+ #
80
+ # @param raw_path [String, nil] a raw PATH_INFO or a configured endpoint
81
+ # @return [String] normalized path suitable for exact literal comparison
82
+ def normalize_path(raw_path)
83
+ raw = raw_path.to_s
84
+ decoded =
85
+ begin
86
+ Rack::Utils.unescape(raw)
87
+ rescue ArgumentError
88
+ raw
89
+ end
90
+ decoded = '/' if decoded.empty?
91
+ decoded
92
+ .encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
93
+ .gsub(%r{/$}, '')
94
+ end
95
+
60
96
  # Validate and normalize an IP address (IPv4 and IPv6).
61
97
  #
62
98
  # Strips an optional port (IPv6-safe), validates with IPAddr, and returns
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.3.1'
6
+ VERSION = '2.4.0'
7
7
  end
data/lib/otto.rb CHANGED
@@ -22,6 +22,7 @@ require_relative 'otto/route_handlers'
22
22
  require_relative 'otto/errors'
23
23
  require_relative 'otto/locale'
24
24
  require_relative 'otto/mcp'
25
+ require_relative 'otto/caddy_tls'
25
26
  require_relative 'otto/core'
26
27
  require_relative 'otto/privacy'
27
28
  require_relative 'otto/security'
@@ -62,6 +63,7 @@ class Otto
62
63
  include Otto::Security::Core
63
64
  include Otto::Privacy::Core
64
65
  include Otto::MCP::Core
66
+ include Otto::CaddyTLS::Core
65
67
 
66
68
  LIB_HOME = __dir__ unless defined?(Otto::LIB_HOME)
67
69
 
@@ -75,7 +77,7 @@ class Otto
75
77
 
76
78
  attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option,
77
79
  :static_route, :security_config, :locale_config, :auth_config,
78
- :route_handler_factory, :mcp_server, :security, :middleware,
80
+ :route_handler_factory, :mcp_server, :caddy_tls_server, :security, :middleware,
79
81
  :error_handlers, :request_class, :response_class
80
82
  attr_accessor :not_found, :server_error
81
83
 
@@ -107,8 +109,13 @@ class Otto
107
109
 
108
110
  # Main Rack application interface
109
111
  def call(env)
110
- # Freeze configuration on first request (thread-safe)
111
- # Skip in test environment to allow test flexibility
112
+ # Freeze configuration on first request (thread-safe).
113
+ # Skipped under RSpec so specs can mutate configuration after construction.
114
+ # Because of this skip, behavior that depends on a genuinely frozen config
115
+ # (e.g. CSP violation dispatch through a frozen Config) is NOT exercised by
116
+ # the normal request path in tests — cover it with specs that freeze
117
+ # explicitly (see spec/otto/security/csp_reporting_frozen_spec.rb and
118
+ # spec/otto/configuration_freezing_spec.rb).
112
119
  unless defined?(RSpec) || @configuration_frozen
113
120
  Otto.logger.debug '[Otto] Lazy freezing check: configuration not yet frozen' if Otto.debug
114
121
 
@@ -169,6 +176,22 @@ class Otto
169
176
  @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' }
170
177
  @security = Otto::Security::Configurator.new(@security_config, @middleware, @auth_config)
171
178
  @app = nil # Pre-built middleware app (built after initialization)
179
+
180
+ # Keep the running Rack app in sync with the middleware stack. This is the
181
+ # SINGLE rebuild trigger: any add/remove/clear — whether via Otto#use,
182
+ # otto.enable_*!, or the otto.security.* Configurator surface — rebuilds @app
183
+ # once it exists. Without it, middleware added through the Configurator after
184
+ # Otto.new would register in the stack but never enter the running request
185
+ # chain, silently disabling CSRF, request validation, rate limiting, and CSP
186
+ # reporting configured that way.
187
+ #
188
+ # The `if @app` guard makes stack mutations during initialization (e.g. the
189
+ # IP-privacy add below) no-ops until the first build_app! runs. A rebuild
190
+ # during a live request could swap @app mid-flight under multi-threaded
191
+ # serving; Otto's contract is to configure before the first request, which
192
+ # the lazy configuration freeze enforces in production.
193
+ @middleware.on_change { build_app! if @app }
194
+
172
195
  @request_complete_callbacks = [] # Instance-level request completion callbacks
173
196
  @route_matched_callbacks = [] # Instance-level route matched callbacks
174
197
  @handler_wrappers = [] # Instance-level handler wrapper factories
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otto
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: concurrent-ruby
@@ -123,12 +124,15 @@ files:
123
124
  - ".github/workflows/claude.yml"
124
125
  - ".github/workflows/code-smells.yml"
125
126
  - ".github/workflows/release-gem.yml"
127
+ - ".github/workflows/ruby-lint.yml"
128
+ - ".github/workflows/yardoc.yml"
126
129
  - ".gitignore"
127
130
  - ".pre-commit-config.yaml"
128
131
  - ".pre-push-config.yaml"
129
132
  - ".reek.yml"
130
133
  - ".rspec"
131
134
  - ".rubocop.yml"
135
+ - ".yardopts"
132
136
  - AGENTS.md
133
137
  - CHANGELOG.rst
134
138
  - Gemfile
@@ -146,6 +150,7 @@ files:
146
150
  - docs/migrating/v2.3.0.md
147
151
  - docs/modern-authentication-authorization-landscape.md
148
152
  - docs/multi-strategy-authentication-design.md
153
+ - docs/reverse-proxy-network-services.md
149
154
  - examples/.gitignore
150
155
  - examples/advanced_routes/README.md
151
156
  - examples/advanced_routes/app.rb
@@ -192,6 +197,11 @@ files:
192
197
  - examples/basic/app.rb
193
198
  - examples/basic/config.ru
194
199
  - examples/basic/routes
200
+ - examples/caddy_tls_demo/README.md
201
+ - examples/caddy_tls_demo/app.rb
202
+ - examples/caddy_tls_demo/config.ru
203
+ - examples/caddy_tls_demo/routes
204
+ - examples/caddy_tls_demo/standalone.ru
195
205
  - examples/error_handler_registration.rb
196
206
  - examples/logging_improvements.rb
197
207
  - examples/mcp_demo/README.md
@@ -204,6 +214,10 @@ files:
204
214
  - examples/security_features/routes
205
215
  - examples/simple_geo_resolver.rb
206
216
  - lib/otto.rb
217
+ - lib/otto/caddy_tls.rb
218
+ - lib/otto/caddy_tls/core.rb
219
+ - lib/otto/caddy_tls/localhost_guard.rb
220
+ - lib/otto/caddy_tls/server.rb
207
221
  - lib/otto/core.rb
208
222
  - lib/otto/core/configuration.rb
209
223
  - lib/otto/core/error_handler.rb
@@ -279,6 +293,10 @@ files:
279
293
  - lib/otto/security/configurator.rb
280
294
  - lib/otto/security/constant_resolver.rb
281
295
  - lib/otto/security/core.rb
296
+ - lib/otto/security/csp.rb
297
+ - lib/otto/security/csp/parser.rb
298
+ - lib/otto/security/csp/report.rb
299
+ - lib/otto/security/csp/report_middleware.rb
282
300
  - lib/otto/security/csrf.rb
283
301
  - lib/otto/security/middleware/csrf_middleware.rb
284
302
  - lib/otto/security/middleware/ip_privacy_middleware.rb
@@ -298,6 +316,7 @@ licenses:
298
316
  - MIT
299
317
  metadata:
300
318
  rubygems_mfa_required: 'true'
319
+ post_install_message:
301
320
  rdoc_options: []
302
321
  require_paths:
303
322
  - lib
@@ -315,7 +334,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
315
334
  - !ruby/object:Gem::Version
316
335
  version: '0'
317
336
  requirements: []
318
- rubygems_version: 3.6.9
337
+ rubygems_version: 3.5.22
338
+ signing_key:
319
339
  specification_version: 4
320
340
  summary: Define your rack-apps in plaintext.
321
341
  test_files: []