otto 2.9.0 → 2.11.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +5 -0
  3. data/.github/workflows/ci.yml +11 -8
  4. data/.github/workflows/claude-code-review.yml +38 -13
  5. data/.github/workflows/claude.yml +10 -8
  6. data/.github/workflows/code-smells.yml +5 -5
  7. data/.github/workflows/release-gem.yml +2 -2
  8. data/.github/workflows/ruby-lint.yml +3 -3
  9. data/.github/workflows/yardoc.yml +5 -5
  10. data/.gitignore +1 -5
  11. data/.rubocop_todo.yml +8 -5
  12. data/AGENTS.md +40 -1
  13. data/CHANGELOG.rst +235 -0
  14. data/Gemfile +3 -3
  15. data/Gemfile.lock +11 -15
  16. data/README.md +75 -37
  17. data/docs/README.md +166 -0
  18. data/docs/adr/README.md +16 -0
  19. data/docs/adr/adr-001-route-authentication-at-handler-boundary.md +38 -0
  20. data/docs/adr/adr-002-multi-strategy-authentication-and-authorization.md +50 -0
  21. data/docs/adr/adr-003-caddy-tls-route-based-integration.md +48 -0
  22. data/docs/adr/adr-004-separate-compatibility-from-security-maintenance.md +58 -0
  23. data/docs/guides/authentication.md +377 -0
  24. data/docs/guides/caddy-tls.md +205 -0
  25. data/docs/guides/configuration_freezing.md +157 -0
  26. data/docs/guides/enrichment.md +161 -0
  27. data/docs/guides/forwarded-authority.md +249 -0
  28. data/docs/guides/geo-country.md +168 -0
  29. data/docs/guides/ip_privacy.md +39 -0
  30. data/docs/guides/ipaddr-encoding-quirk.md +56 -0
  31. data/docs/guides/mcp.md +282 -0
  32. data/docs/guides/privacy.md +193 -0
  33. data/docs/guides/routing.md +293 -0
  34. data/docs/guides/structured_logging.md +281 -0
  35. data/docs/guides/testing-guide.md +391 -0
  36. data/docs/maintainers/github-actions.md +41 -0
  37. data/docs/maintainers/investigations/.gitignore +2 -0
  38. data/docs/migrating/v2.0.0.md +337 -0
  39. data/docs/reference/authentication.md +290 -0
  40. data/docs/reference/route-syntax.md +181 -0
  41. data/docs/reference/runtime-and-dependency-security.md +86 -0
  42. data/examples/advanced_routes/README.md +43 -57
  43. data/examples/authentication_strategies/README.md +37 -196
  44. data/examples/basic/README.md +24 -39
  45. data/examples/basic/config.ru +0 -1
  46. data/examples/caddy_tls_demo/README.md +8 -2
  47. data/examples/lambda_handlers/README.md +11 -2
  48. data/examples/mcp_demo/README.md +75 -161
  49. data/examples/mcp_demo/config.ru +1 -0
  50. data/examples/security_features/README.md +36 -234
  51. data/lib/otto/caddy_tls/localhost_guard.rb +22 -24
  52. data/lib/otto/core/configuration.rb +40 -23
  53. data/lib/otto/core/error_handler.rb +40 -2
  54. data/lib/otto/core/file_safety.rb +105 -31
  55. data/lib/otto/core/middleware_stack.rb +36 -36
  56. data/lib/otto/core/router.rb +94 -18
  57. data/lib/otto/core/static_mounts.rb +172 -0
  58. data/lib/otto/core.rb +1 -0
  59. data/lib/otto/env_keys.rb +20 -2
  60. data/lib/otto/mcp/auth/token.rb +10 -4
  61. data/lib/otto/mcp/core.rb +23 -5
  62. data/lib/otto/mcp/endpoint.rb +41 -0
  63. data/lib/otto/mcp/errors.rb +15 -0
  64. data/lib/otto/mcp/options.rb +292 -0
  65. data/lib/otto/mcp/protocol.rb +52 -22
  66. data/lib/otto/mcp/rate_limiting.rb +175 -97
  67. data/lib/otto/mcp/registry.rb +14 -14
  68. data/lib/otto/mcp/schema_validation.rb +20 -12
  69. data/lib/otto/mcp/server.rb +131 -32
  70. data/lib/otto/optional_dependency.rb +57 -0
  71. data/lib/otto/privacy/config.rb +10 -8
  72. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  73. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  74. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  75. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  76. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  77. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  78. data/lib/otto/security/config.rb +328 -14
  79. data/lib/otto/security/configurator.rb +36 -6
  80. data/lib/otto/security/core.rb +19 -1
  81. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  82. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  83. data/lib/otto/security/rate_limiter.rb +81 -46
  84. data/lib/otto/static.rb +38 -0
  85. data/lib/otto/utils.rb +50 -0
  86. data/lib/otto/version.rb +1 -1
  87. data/lib/otto.rb +130 -13
  88. data/otto.gemspec +0 -2
  89. metadata +32 -41
  90. data/docs/.gitignore +0 -10
  91. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  92. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  93. data/docs/enrichment.md +0 -128
  94. data/docs/geo-country.md +0 -181
  95. data/docs/ipaddr-encoding-quirk.md +0 -34
  96. data/docs/migrating/v2.0.0-pre1.md +0 -276
  97. data/docs/migrating/v2.0.0-pre2.md +0 -338
  98. data/docs/modern-authentication-authorization-landscape.md +0 -558
  99. data/docs/multi-strategy-authentication-design.md +0 -1401
  100. data/docs/reverse-proxy-network-services.md +0 -371
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,203 @@ 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.11.0:
11
+
12
+ 2.11.0 — 2026-09-12
13
+ ===================
14
+
15
+ Added
16
+ -----
17
+
18
+ - ``Otto#mount_static(prefix, root:)`` serves an explicit directory at a URL
19
+ prefix. See ``docs/guides/routing.md`` for configuration, dispatch
20
+ precedence, and migration from ``add_static_path``. (#267)
21
+
22
+ - ``Otto#not_found=`` and ``Otto#server_error=`` now accept callables for
23
+ per-request fallback responses. A server-error callable can receive the
24
+ exception; see ``docs/guides/routing.md`` for the callback contract. (#272)
25
+
26
+ Security
27
+ --------
28
+
29
+ - Static Rack triples configured with ``Otto#not_found=`` or
30
+ ``Otto#server_error=`` are now copied for each request, preventing in-place
31
+ header changes, including ``Set-Cookie``, from being shared between fallback
32
+ responses. (#272)
33
+
34
+ Documentation
35
+ -------------
36
+
37
+ - Documented static-file dispatch precedence and migration from the removed
38
+ ``add_static_path`` API in ``docs/guides/routing.md``. Corrected stale
39
+ ``routes_static`` cache guidance in ``docs/guides/configuration_freezing.md``.
40
+ (#267)
41
+
42
+ .. _changelog-2.10.0:
43
+
44
+ 2.10.0 — 2026-09-04
45
+ ===================
46
+
47
+ Added
48
+ -----
49
+
50
+ - ``APIKeyStrategy`` now accepts a block or ``resolver:`` callable instead of a
51
+ static ``api_keys:`` list, enabling database-, repository-, or cache-backed
52
+ key lookup. The resolved account becomes the authenticated user.
53
+
54
+ - ``APIKeyStrategy.digest(key)`` returns a full SHA-256 hex digest for stores
55
+ that look up generated API keys by digest. See
56
+ ``docs/guides/authentication.md`` for the resolver contract and credential
57
+ storage guidance.
58
+
59
+ - Directly exposed applications can now set ``trusted_proxies: :none`` or call
60
+ ``trust_no_proxies!`` to distrust every peer. Otto then ignores forwarded
61
+ client IPs and strips forwarded host, scheme, and port metadata. Leaving proxy
62
+ trust unconfigured continues to preserve forwarded metadata. Applications
63
+ behind a reverse proxy, including one on loopback, must explicitly trust it.
64
+ The sentinel is only valid as the whole option; a list containing it, such
65
+ as ``['none']``, is rejected at configuration time. See
66
+ ``docs/guides/forwarded-authority.md`` for configuration guidance. (#259)
67
+
68
+ Changed
69
+ -------
70
+
71
+ - MCP options now accept String or Symbol keys consistently and fail at boot
72
+ for unknown, conflicting, or invalid values. Constructor gating options must
73
+ be exactly ``true`` or ``false``; nil or blank token values are rejected.
74
+ Before upgrading, check option names against the `MCP guide
75
+ <docs/guides/mcp.md>`__. (#258)
76
+
77
+ - ``enable_mcp!`` now rejects constructor-only gating options and repeated
78
+ enablement. Pass all MCP settings in one ``Otto.new`` or ``enable_mcp!`` call.
79
+ (#258)
80
+
81
+ - MCP ``Rack::Attack`` throttle names are now endpoint-qualified, such as
82
+ ``mcp_requests:/_mcp`` and ``mcp_tool_calls:/_mcp``. Update integrations that
83
+ inspect throttle names directly. (#258)
84
+
85
+ - Otto applications in one process that resolve proxied requests must now use
86
+ the same forwarded-header family; incompatible configurations fail during
87
+ configuration. CIDR-based proxy trust supports only ``X-Forwarded-*`` headers; use
88
+ depth-based trust for ``Forwarded`` or both families. See
89
+ ``docs/guides/forwarded-authority.md`` for configuration guidance. (#252)
90
+
91
+ - Literal routes now consistently take precedence over static files at the same
92
+ path, regardless of which static files were requested earlier. Static files
93
+ continue to take precedence over dynamic routes. (#260)
94
+
95
+ - MaxMind-backed database paths now require ``maxmind-db ~> 1.2``. Before
96
+ upgrading, declare that range or provide a compatible reader object. See the
97
+ `geo-country <docs/guides/geo-country.md>`__ and
98
+ `enrichment <docs/guides/enrichment.md>`__ guides. (#255)
99
+
100
+ Removed
101
+ -------
102
+
103
+ - Removed ``add_static_path`` and the ``routes_static`` cache interface. Static
104
+ files are discovered directly from the configured ``public`` directory and
105
+ no longer need registration. (#260)
106
+
107
+ - Removed the unused ``rack-parser`` and ``rexml`` runtime dependencies.
108
+ Applications using either gem directly must now declare it themselves.
109
+ (#255)
110
+
111
+ Fixed
112
+ -----
113
+
114
+ - MCP middleware now executes rate limiting, authentication, and schema
115
+ validation in that order. Configured endpoints with a trailing slash are also
116
+ routable. (#258)
117
+
118
+ - Repeated MCP rate-limit configuration no longer produces duplicate throttle
119
+ log entries. (#258)
120
+
121
+ - MCP JSON-RPC errors now consistently use HTTP 400 for protocol errors, 404
122
+ for unknown resources or tools, and 500 for handler or server failures.
123
+ Unknown tools no longer return 500, failing resource handlers no longer
124
+ appear missing, and handler exception details are logged instead of returned
125
+ to clients. (#257)
126
+
127
+ - ``StrategyResult#has_role?`` and ``#has_permission?`` now derive their answer
128
+ from ``#roles`` and ``#permissions``, so the predicates agree with the
129
+ accessors for object-backed users exposing ``#roles`` and for ``Set`` or
130
+ other non-Array collections. A user model defining its own ``#has_role?`` or
131
+ ``#has_permission?`` is still consulted first.
132
+
133
+ - MCP rate-limited requests now emit one ``rack.attack`` log entry instead of
134
+ two. (#255)
135
+
136
+ Security
137
+ --------
138
+
139
+ - ``APIKeyStrategy`` now requires exactly one key source and rejects empty
140
+ static key lists at construction. Invalid presented credentials terminate a
141
+ multi-strategy authentication chain. Static keys are compared in constant
142
+ time as fixed-width SHA-256 digests, so configured key lengths are not
143
+ observable through timing. Blank credentials, empty or whitespace-only, are
144
+ rejected before any key source is consulted. (#256)
145
+
146
+ - Successful authentication results no longer expose the raw API key in
147
+ strategy-generated fields. Static-list callers should replace
148
+ ``user[:api_key]`` with ``user[:api_key_fingerprint]``. (#256)
149
+
150
+ - ``APIKeyStrategy`` now reads only the configured header by default. Applications
151
+ that still accept query or form parameters must opt in with ``param_name:``;
152
+ see ``docs/guides/authentication.md`` for configuration guidance. (#256)
153
+
154
+ - Fixed MCP bearer-token authentication being silently omitted when configured
155
+ through ``Otto.new`` or with String-keyed options. Deployments exposing MCP
156
+ beyond a trusted local environment should upgrade and verify their
157
+ ``auth_tokens`` configuration. Intentionally open endpoints now warn unless
158
+ acknowledged with ``allow_unauthenticated: true``. See the `MCP guide
159
+ <docs/guides/mcp.md>`__. (#258)
160
+
161
+ - MCP authentication now fails closed when its authenticator is unavailable.
162
+ Authentication, validation, and rate limiting also apply only to the exact
163
+ routed MCP endpoint. (#258)
164
+
165
+ - MCP rate limiting now honors configured limits and protects custom endpoints,
166
+ Rack-mounted applications, and distinct MCP endpoints in the same process.
167
+ Mount ``Rack::Attack`` inside the same ``map`` block as Otto, and use distinct
168
+ endpoint paths when applications require isolated counters. (#258)
169
+
170
+ - Static-file serving now rejects symlinks that resolve outside the configured
171
+ public directory, preventing files outside that root from being served on
172
+ first access or after another file has populated the directory cache.
173
+ Symlinks that resolve within the public directory, including a symlinked
174
+ public root, remain supported. (#257)
175
+
176
+ - When proxy trust is configured, forwarded host, scheme, and port metadata
177
+ from untrusted peers is now stripped before it can affect Rack's request
178
+ authority. Requests from trusted peers retain this metadata; see
179
+ ``docs/guides/forwarded-authority.md`` for deployment guidance. (#252)
180
+
181
+ - ``IPPrivacyMiddleware`` enforces its own proxy trust posture even when an
182
+ outer instance already resolved ``otto.client_ip``. ``trusted_proxies: :none``
183
+ always strips forwarded authority; a CIDR configuration that can no longer
184
+ match the connecting peer treats it as untrusted and logs a warning. (#259)
185
+
186
+ - ``env['otto.peer_relayed']`` records whether a request carried any relay
187
+ marker header, evaluated before forwarded carriers may be deleted, so
188
+ ``Otto::CaddyTLS::LocalhostGuard`` still refuses a relayed loopback call once
189
+ the carriers are stripped. The relay markers cover every carrier the scrub
190
+ deletes, including ``X-Forwarded-Host`` and the other authority headers, not
191
+ only the client-IP carriers. (#259)
192
+
193
+ - MCP now stops configuration with ``Otto::OptionalDependencyError`` rather
194
+ than starting without enabled schema validation or rate limiting. Before
195
+ upgrading, add ``json_schemer ~> 2.0`` and ``rack-attack ~> 6.7`` for the
196
+ default protections, or explicitly set ``enable_validation: false`` or
197
+ ``enable_rate_limiting: false`` only when that protection is not required.
198
+ See the `MCP guide <docs/guides/mcp.md>`__. (#255)
199
+
200
+ Documentation
201
+ -------------
202
+
203
+ - Added the `MCP guide <docs/guides/mcp.md>`__ covering secure enablement,
204
+ supported options, authentication, validation, rate limiting, mounted
205
+ applications, and error behavior. (#258)
206
+
10
207
  .. _changelog-2.9.0:
11
208
 
12
209
  2.9.0 — 2026-08-18
@@ -790,6 +987,44 @@ Added
790
987
  2.0.0 — 2026-03-14
791
988
  ==================
792
989
 
990
+ This is the stable release of Otto v2, the culmination of 10 pre-releases
991
+ since September 2025.
992
+
993
+ Highlights
994
+ ----------
995
+
996
+ - **Modular architecture**: the core ``Otto`` class is now a thin composition
997
+ of focused modules (Router, FileSafety, Configuration, ErrorHandler,
998
+ UriGenerator).
999
+ - **Security by default**: IP masking, user agent anonymization, CSRF
1000
+ protection, input validation, and backtrace sanitization.
1001
+ - **Privacy by default**: public IP masking, country-level geo-location only
1002
+ (no external APIs), daily-rotating IP hashes for analytics.
1003
+ - **Handler-level authentication**: authentication moved from middleware to
1004
+ ``RouteAuthWrapper``, so it runs after routing.
1005
+ - **Configuration freezing**: configuration is frozen after the first request
1006
+ to prevent runtime security bypasses.
1007
+ - **MCP support**: JSON-RPC 2.0 endpoints for CLI automation and integrations.
1008
+ - **Base error classes**: ``NotFoundError``, ``BadRequestError``,
1009
+ ``ForbiddenError`` and friends, with automatic HTTP status codes.
1010
+ - **Request/response helpers**: extensible ``Otto::Request`` and
1011
+ ``Otto::Response`` with application-specific helper registration.
1012
+
1013
+ Breaking changes
1014
+ ----------------
1015
+
1016
+ Individual breaking changes are documented in the pre-release entries below.
1017
+ The migrations most applications need:
1018
+
1019
+ - Logic class constructor: ``initialize(session, user, params, locale)`` →
1020
+ ``initialize(context, params, locale)``
1021
+ - Middleware stack: ``otto.middleware_stack <<`` → ``otto.use()``
1022
+ - Request callbacks: ``Otto.on_request_complete`` → ``otto.on_request_complete``
1023
+ (instance method)
1024
+
1025
+ See `docs/migrating/v2.0.0.md <docs/migrating/v2.0.0.md>`__ for the full
1026
+ upgrade guide.
1027
+
793
1028
  Added
794
1029
  -----
795
1030
 
data/Gemfile CHANGED
@@ -18,9 +18,9 @@ end
18
18
  # bundle config set with 'optional'
19
19
  group :development, :test, optional: true do
20
20
  # Keep gems that need to be in both environments
21
- gem 'json_schemer'
21
+ gem 'json_schemer', '~> 2.0'
22
22
  gem 'maxmind-db', '~> 1.2' # Optional geo DB reader; exercised by geo specs
23
- gem 'rack-attack'
23
+ gem 'rack-attack', '~> 6.7'
24
24
  gem 'reek', '~> 6.5'
25
25
  end
26
26
 
@@ -29,7 +29,7 @@ group :development do
29
29
  gem 'debug'
30
30
  gem 'rackup' # Used to boot examples/ apps; not needed by specs
31
31
  gem 'rake', '~> 13.4', require: false # Provides `rake release` for release-gem.yml
32
- gem 'rubocop', '~> 1.89.0', require: false
32
+ gem 'rubocop', '~> 1.90.0', require: false
33
33
  gem 'rubocop-performance', require: false
34
34
  gem 'rubocop-rspec', require: false
35
35
  gem 'rubocop-thread_safety', require: false
data/Gemfile.lock CHANGED
@@ -1,13 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (2.9.0)
4
+ otto (2.11.0)
5
5
  concurrent-ruby (~> 1.3, < 2.0)
6
6
  logger (~> 1, < 2.0)
7
7
  loofah (~> 2.20)
8
8
  rack (~> 3.1, < 4.0)
9
- rack-parser (~> 0.7)
10
- rexml (~> 3.4)
11
9
 
12
10
  GEM
13
11
  remote: https://rubygems.org/
@@ -106,18 +104,16 @@ GEM
106
104
  date
107
105
  stringio
108
106
  racc (1.8.1)
109
- rack (3.2.6)
107
+ rack (3.2.7)
110
108
  rack-attack (6.8.0)
111
109
  rack (>= 1.0, < 4)
112
- rack-parser (0.7.0)
113
- rack
114
110
  rack-test (2.2.0)
115
111
  rack (>= 1.3)
116
112
  rackup (2.3.1)
117
113
  rack (>= 3)
118
114
  rainbow (3.1.1)
119
115
  rake (13.4.2)
120
- rbs (4.0.3)
116
+ rbs (4.1.3)
121
117
  logger
122
118
  prism (>= 1.6.0)
123
119
  tsort
@@ -148,8 +144,8 @@ GEM
148
144
  diff-lcs (>= 1.2.0, < 2.0)
149
145
  rspec-support (~> 3.13.0)
150
146
  rspec-support (3.13.7)
151
- rubocop (1.89.0)
152
- json (~> 2.3)
147
+ rubocop (1.90.0)
148
+ json (>= 2.3)
153
149
  language_server-protocol (~> 3.17.0.2)
154
150
  lint_roller (~> 1.1.0)
155
151
  parallel (>= 1.10)
@@ -162,9 +158,9 @@ GEM
162
158
  rubocop-ast (1.50.0)
163
159
  parser (>= 3.3.7.2)
164
160
  prism (~> 1.7)
165
- rubocop-performance (1.26.1)
161
+ rubocop-performance (1.27.0)
166
162
  lint_roller (~> 1.1)
167
- rubocop (>= 1.75.0, < 2.0)
163
+ rubocop (>= 1.89.0, < 2.0)
168
164
  rubocop-ast (>= 1.47.1, < 2.0)
169
165
  rubocop-rspec (3.10.2)
170
166
  lint_roller (~> 1.1)
@@ -174,7 +170,7 @@ GEM
174
170
  lint_roller (~> 1.1)
175
171
  rubocop (~> 1.72, >= 1.72.1)
176
172
  rubocop-ast (>= 1.44.0, < 2.0)
177
- ruby-lsp (0.26.10)
173
+ ruby-lsp (0.26.11)
178
174
  language_server-protocol (~> 3.17.0)
179
175
  prism (>= 1.2, < 2.0)
180
176
  rbs (>= 3, < 5)
@@ -217,17 +213,17 @@ PLATFORMS
217
213
  DEPENDENCIES
218
214
  benchmark
219
215
  debug
220
- json_schemer
216
+ json_schemer (~> 2.0)
221
217
  kramdown
222
218
  maxmind-db (~> 1.2)
223
219
  otto!
224
- rack-attack
220
+ rack-attack (~> 6.7)
225
221
  rack-test
226
222
  rackup
227
223
  rake (~> 13.4)
228
224
  reek (~> 6.5)
229
225
  rspec (~> 3.13)
230
- rubocop (~> 1.89.0)
226
+ rubocop (~> 1.90.0)
231
227
  rubocop-performance
232
228
  rubocop-rspec
233
229
  rubocop-thread_safety
data/README.md CHANGED
@@ -1,22 +1,41 @@
1
- # Otto - A Ruby Gem
1
+ # Otto - All Rack, no Pinion
2
2
 
3
- **Define your rack-apps in plain-text with built-in security.**
4
-
5
- > **v2.0.0-pre6 Available**: This pre-release includes major improvements to middleware management, logging, and request callback handling. See [changelog](CHANGELOG.rst) for details and upgrade notes.
3
+ **Define Rack apps in plain text, with privacy by default and opt-in security features.**
6
4
 
7
5
  ![Otto mascot](public/img/otto.jpg "Otto - All Rack, no Pinion")
8
6
 
9
- Otto apps have three files: a rackup file, a Ruby class, and a routes file. The routes file is just plain text that maps URLs to Ruby methods.
7
+ Otto apps have three files: a rackup file, a Ruby class, and a routes file. The routes file is plain text that maps URLs to Ruby methods.
8
+
9
+ ## Quick start
10
+
11
+ Requirements: Ruby `>= 3.2, < 4.1` and Rack `>= 3.1, < 4.0`. Ruby 3.2
12
+ remains compatibility-tested after upstream end of life, but receives no
13
+ interpreter security maintenance. See the [runtime and dependency security
14
+ policy](docs/reference/runtime-and-dependency-security.md) for support tiers and
15
+ consumer lockfile requirements.
10
16
 
11
- ```bash
12
- $ cd myapp && ls
13
- config.ru app.rb routes
17
+ Install Otto and the Rack server CLI, then create the three files shown below:
18
+
19
+ ```sh
20
+ gem install otto rackup
21
+ mkdir myapp && cd myapp
22
+ ```
23
+
24
+ After creating `routes`, `app.rb`, and `config.ru`, start the app and verify the response:
25
+
26
+ ```sh
27
+ rackup config.ru
28
+ # In another terminal:
29
+ curl -i http://127.0.0.1:9292/
30
+ # HTTP/1.1 200 OK
31
+ # ...
32
+ # <h1>Hello Otto</h1>
14
33
  ```
15
34
 
16
35
  ## Why Otto?
17
36
 
18
- - **Security by Default**: Automatic IP masking for public addresses, user agent anonymization, CSRF protection, and input validation
19
- - **Privacy First**: Masks public IPs, strips user agent versions, provides country-level geo-location only—no external APIs needed
37
+ - **Privacy by Default**: Masks public IP addresses and anonymizes user agents; country-level geo-location needs no external API
38
+ - **Opt-in Security Features**: CSRF protection, input validation, security headers, and trusted-proxy configuration
20
39
  - **Simple Routing**: Define routes in plain-text files with zero configuration overhead
21
40
  - **Built-in Authentication**: Multiple strategies including API keys, tokens, role-based access, and custom implementations
22
41
  - **Developer Friendly**: Works with any Rack server, minimal dependencies, easy testing and debugging
@@ -26,10 +45,8 @@ config.ru app.rb routes
26
45
  # routes
27
46
 
28
47
  GET / App#index
29
- POST /feedback App#receive_feedback
30
48
  GET /product/:id App#show_product
31
49
  GET /robots.txt App#robots_text
32
- GET /404 App#not_found
33
50
  ```
34
51
 
35
52
  ## Ruby Class
@@ -37,6 +54,8 @@ GET /404 App#not_found
37
54
  # app.rb
38
55
 
39
56
  class App
57
+ attr_reader :req, :res
58
+
40
59
  def initialize(req, res)
41
60
  @req, @res = req, res
42
61
  end
@@ -51,7 +70,7 @@ class App
51
70
  end
52
71
 
53
72
  def robots_text
54
- res.header['Content-Type'] = "text/plain"
73
+ res.headers['content-type'] = 'text/plain'
55
74
  rules = 'User-agent: *', 'Disallow: /private/keep/out'
56
75
  res.body = rules.join($/)
57
76
  end
@@ -63,9 +82,9 @@ end
63
82
  # config.ru
64
83
 
65
84
  require 'otto'
66
- require 'app'
85
+ require_relative 'app'
67
86
 
68
- run Otto.new("./routes")
87
+ run Otto.new('routes')
69
88
  ```
70
89
 
71
90
 
@@ -82,7 +101,27 @@ app = Otto.new("./routes", {
82
101
  })
83
102
  ```
84
103
 
85
- Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
104
+ Security features include CSRF protection, input validation, security headers, rate limiting, and trusted proxy configuration.
105
+
106
+ ### Rate limiting (`rack-attack`)
107
+
108
+ Rate limiting requires `rack-attack` 6.7.0 or newer in the 6.x series. Add the
109
+ optional dependency and mount it before Otto; `enable_rate_limiting!` configures
110
+ rules, while `Rack::Attack` enforces them:
111
+
112
+ ```ruby
113
+ # Gemfile
114
+ gem 'rack-attack', '~> 6.7'
115
+
116
+ # config.ru
117
+ use Rack::Attack
118
+ app = Otto.new('./routes')
119
+ app.enable_rate_limiting!(requests_per_minute: 50)
120
+ run app
121
+ ```
122
+
123
+ Enabling rate limiting raises `Otto::OptionalDependencyError` at configuration
124
+ time when the gem is missing or outside the supported range.
86
125
 
87
126
  ### Content Security Policy (nonce-based emission)
88
127
 
@@ -300,7 +339,7 @@ Private and localhost IPs are exempted by default for development convenience, b
300
339
  ```ruby
301
340
  otto.configure_ip_privacy(
302
341
  geo_header: 'X-Client-Country', # trusted app header, checked first
303
- geo_db_path: 'data/country.mmdb' # offline fallback (needs the maxmind-db gem)
342
+ geo_db_path: 'data/country.mmdb' # offline fallback (needs maxmind-db >= 1.2.0, < 2)
304
343
  )
305
344
  ```
306
345
 
@@ -395,8 +434,9 @@ on_demand_tls {
395
434
 
396
435
  Secure by default: the endpoint is restricted to the loopback interface (the guard
397
436
  authenticates the raw TCP peer, so a spoofed `X-Forwarded-For` cannot help), and
398
- every layer fails closed. See [docs/reverse-proxy-network-services.md](docs/reverse-proxy-network-services.md)
399
- for the design and deployment notes.
437
+ every layer fails closed. See the [Caddy TLS guide](docs/guides/caddy-tls.md)
438
+ for deployment guidance and [ADR-003](docs/adr/adr-003-caddy-tls-route-based-integration.md)
439
+ for rationale.
400
440
 
401
441
  ## Examples
402
442
 
@@ -406,7 +446,7 @@ Otto includes comprehensive examples demonstrating different features:
406
446
  - **[Advanced Routes](examples/advanced_routes/)** - Response types, CSRF exemption, logic classes, and namespaced routing
407
447
  - **[Authentication Strategies](examples/authentication_strategies/)** - Token, API key, and role-based authentication
408
448
  - **[Security Features](examples/security_features/)** - CSRF protection, input validation, file uploads, and security headers
409
- - **[MCP Demo](examples/mcp_demo/)** - JSON-RPC 2.0 endpoints for CLI automation and integrations
449
+ - **[MCP Demo](examples/mcp_demo/)** - JSON-RPC 2.0 endpoints for CLI automation and integrations (see the [MCP guide](docs/guides/mcp.md))
410
450
  - **[Caddy on-demand TLS](examples/caddy_tls_demo/)** - Reverse-proxy permission endpoint via `Otto::CaddyTLS`
411
451
 
412
452
  ### Standalone Tutorials
@@ -417,32 +457,30 @@ Otto includes comprehensive examples demonstrating different features:
417
457
 
418
458
  See the [examples/](examples/) directory for more.
419
459
 
420
- ## Requirements
421
460
 
422
- - Ruby 3.2+
423
- - Rack 3.1+
461
+ ## Documentation
424
462
 
425
- ## Installation
463
+ - **[Documentation map](docs/README.md)** - Capabilities, current guides, and the documentation structure Otto is growing toward
464
+ - **[Runtime and dependency security policy](docs/reference/runtime-and-dependency-security.md)** - Ruby compatibility, dependency-range guarantees, and consumer lockfile auditing
465
+ - **[CHANGELOG.rst](CHANGELOG.rst)** - Version history, breaking changes, and upgrade notes
426
466
 
427
- ```bash
428
- gem install otto
429
- ```
467
+ ## AI-assisted development
430
468
 
431
- ## Documentation
469
+ AI tools have contributed to Otto since v1.2.0. They are used for implementation, testing, documentation, maintenance, and additional code review.
432
470
 
433
- - **[AGENTS.md](AGENTS.md)** - Comprehensive developer guidance covering authentication architecture, configuration freezing, IP privacy, structured logging, and multi-app patterns
434
- - **[docs/](docs/)** - Technical guides and migration guides
435
- - **[CHANGELOG.rst](CHANGELOG.rst)** - Version history, breaking changes, and upgrade notes
471
+ ### Tools used
436
472
 
437
- ## AI Development Assistance
473
+ * **Claude Code CLI (Opus and Fable)** - Primary development tool for implementation, test coverage, and adversarial review
474
+ * **Greptile and GitHub Copilot** - Pull request reviews
475
+ * **Claude Desktop routines** - Unattended cloud agents that run on a cron schedule and open pull requests for maintenance chores. Nobody is at the keyboard for these, so they go through the same PR review as human work.
438
476
 
439
- Version 1.2.0's security features were developed with AI assistance:
477
+ ### How it is tracked
440
478
 
441
- * **Zed Agent (Claude Sonnet 4)** - Security implementation and testing
442
- * **Claude Desktop** - Rack 3+ compatibility and debugging
443
- * **GitHub Copilot** - Code completion
479
+ * All PR reviews are publicly visible. The `greptile-review` and `claude-review` labels are applied to PRs that have been reviewed.
480
+ * Release notes in [CHANGELOG.rst](CHANGELOG.rst) list AI-assisted work under an "AI Assistance" heading.
481
+ * [AGENTS.md](AGENTS.md) sets the conventions agents follow in this repository.
444
482
 
445
- The maintainer remains responsible for all security decisions and implementation. We believe in transparency about development tools, especially for security-focused software.
483
+ The maintainer reviews the resulting changes and remains responsible for security, implementation, and releases.
446
484
 
447
485
  ## License
448
486