otto 2.9.0 → 2.10.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 (96) 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 +7 -5
  12. data/AGENTS.md +22 -1
  13. data/CHANGELOG.rst +203 -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 +146 -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 +181 -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 +36 -22
  53. data/lib/otto/core/file_safety.rb +89 -31
  54. data/lib/otto/core/middleware_stack.rb +36 -36
  55. data/lib/otto/core/router.rb +62 -19
  56. data/lib/otto/env_keys.rb +20 -2
  57. data/lib/otto/mcp/auth/token.rb +10 -4
  58. data/lib/otto/mcp/core.rb +23 -5
  59. data/lib/otto/mcp/endpoint.rb +41 -0
  60. data/lib/otto/mcp/errors.rb +15 -0
  61. data/lib/otto/mcp/options.rb +292 -0
  62. data/lib/otto/mcp/protocol.rb +52 -22
  63. data/lib/otto/mcp/rate_limiting.rb +175 -97
  64. data/lib/otto/mcp/registry.rb +14 -14
  65. data/lib/otto/mcp/schema_validation.rb +20 -12
  66. data/lib/otto/mcp/server.rb +131 -32
  67. data/lib/otto/optional_dependency.rb +57 -0
  68. data/lib/otto/privacy/config.rb +10 -8
  69. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  70. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  71. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  72. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  73. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  74. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  75. data/lib/otto/security/config.rb +328 -14
  76. data/lib/otto/security/configurator.rb +36 -6
  77. data/lib/otto/security/core.rb +19 -1
  78. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  79. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  80. data/lib/otto/security/rate_limiter.rb +81 -46
  81. data/lib/otto/utils.rb +50 -0
  82. data/lib/otto/version.rb +1 -1
  83. data/lib/otto.rb +9 -11
  84. data/otto.gemspec +0 -2
  85. metadata +31 -41
  86. data/docs/.gitignore +0 -10
  87. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  88. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  89. data/docs/enrichment.md +0 -128
  90. data/docs/geo-country.md +0 -181
  91. data/docs/ipaddr-encoding-quirk.md +0 -34
  92. data/docs/migrating/v2.0.0-pre1.md +0 -276
  93. data/docs/migrating/v2.0.0-pre2.md +0 -338
  94. data/docs/modern-authentication-authorization-landscape.md +0 -558
  95. data/docs/multi-strategy-authentication-design.md +0 -1401
  96. data/docs/reverse-proxy-network-services.md +0 -371
@@ -0,0 +1,181 @@
1
+ # Route syntax
2
+
3
+ This page is the compact contract for Otto's plain-text route definitions. It
4
+ covers the syntax needed to choose a route target and attach route options. For
5
+ application examples, start with the [routing guide](../guides/routing.md).
6
+
7
+ ## Route line
8
+
9
+ A route line has this shape:
10
+
11
+ ```text
12
+ VERB /path-pattern Target option=value option=value
13
+ ```
14
+
15
+ For example:
16
+
17
+ ```text
18
+ GET / App#index
19
+ GET /products/:id App#show_product response=view
20
+ POST /api/products ProductCreate response=json csrf=exempt
21
+ GET /admin Admin::Dashboard auth=session role=admin
22
+ GET /health &health_check
23
+ ```
24
+
25
+ Whitespace separates the verb, path, target, and each option token. Option
26
+ values cannot contain unquoted whitespace. Values may contain `=` characters;
27
+ Otto splits each option at its first `=`.
28
+
29
+ ## HTTP verbs and paths
30
+
31
+ The verb is normalized to uppercase. A path can contain named segments and a
32
+ splat:
33
+
34
+ ```text
35
+ GET /products/:id Products#show
36
+ GET /assets/*path Assets#show
37
+ ```
38
+
39
+ Named segments become route parameters. The splat is available under the
40
+ `"splat"` parameter name. Route matching is anchored to the complete path, and
41
+ Otto normalizes paths before matching, including trailing slashes and URL
42
+ encoding according to the router's path-normalization rules.
43
+
44
+ ## Targets
45
+
46
+ The target determines how Otto invokes application code:
47
+
48
+ | Syntax | Kind | Invocation |
49
+ | --- | --- | --- |
50
+ | `App.index` | Class method | Calls `App.index(req, res)`. |
51
+ | `App#index` | Instance method | Builds `App.new(req, res)`, then calls `index`. |
52
+ | `Admin::Dashboard` | Logic class | Builds `Admin::Dashboard.new(strategy_result, params, locale)`, then runs its Logic lifecycle. |
53
+ | `&health_check` | Registered lambda | Looks up `"health_check"` in the boot-time `lambda_handlers` registry and calls it with `(req, res, extra_params)`. |
54
+
55
+ Class and instance targets must resolve to safe Ruby constants. Lambda targets
56
+ are registry keys, not Ruby constants: Otto does not evaluate route text or
57
+ resolve a lambda name dynamically.
58
+
59
+ ### Logic classes
60
+
61
+ A bare class target is a Logic route. Logic classes receive a constrained
62
+ application context rather than the Rack request and environment:
63
+
64
+ ```ruby
65
+ class ProductShow
66
+ def initialize(strategy_result, params, locale)
67
+ @context = strategy_result
68
+ @params = params
69
+ @locale = locale
70
+ end
71
+
72
+ def raise_concerns
73
+ # Load resources and perform resource-level authorization here.
74
+ end
75
+
76
+ def process
77
+ { id: @params[:id], locale: @locale }
78
+ end
79
+ end
80
+ ```
81
+
82
+ The route handler supplies the authenticated `StrategyResult` even for a public
83
+ route; public routes receive an anonymous result. Use a class or instance
84
+ handler when code needs direct access to cookies, headers, or the Rack request.
85
+
86
+ ### Registered lambda handlers
87
+
88
+ Register lambdas before the first request:
89
+
90
+ ```ruby
91
+ otto = Otto.new('routes', lambda_handlers: {
92
+ health_check: lambda do |req, res, _extra_params|
93
+ res['content-type'] = 'text/plain'
94
+ res.body = 'ok'
95
+ end,
96
+ })
97
+ ```
98
+
99
+ The callable must accept three positional arguments. Registration validates the
100
+ name and callable arity at boot; a route referring to an unregistered handler
101
+ fails when it is invoked rather than evaluating arbitrary route text.
102
+
103
+ ## Route options
104
+
105
+ Options are whitespace-delimited `key=value` tokens after the target.
106
+ Unknown well-formed options are retained for handlers and middleware that use
107
+ them. Malformed non-security options are ignored and logged.
108
+
109
+ ### Response type
110
+
111
+ `response=` selects the response handler for non-default handler results:
112
+
113
+ | Option | Behavior |
114
+ | --- | --- |
115
+ | `response=default` | Default response behavior. This is the default. |
116
+ | `response=json` | JSON response handling. |
117
+ | `response=view` | View response handling. |
118
+ | `response=redirect` | Redirect response handling. |
119
+ | `response=auto` | Content-negotiated response handling. |
120
+
121
+ An unknown response type falls back to the default handler silently. A typo in
122
+ `response=` therefore changes behavior with no diagnostic; check the value here
123
+ rather than expecting a log line.
124
+
125
+ ### CSRF
126
+
127
+ When CSRF protection is enabled, use the explicit exemption only for routes
128
+ whose request contract does not use browser credentials and therefore has an
129
+ independent protection model:
130
+
131
+ ```text
132
+ POST /webhooks/provider Webhooks#receive csrf=exempt
133
+ ```
134
+
135
+ `csrf=exempt` is the supported exemption token. A bare `csrf` or an empty CSRF
136
+ value is rejected as a malformed security option.
137
+
138
+ ### Authentication and roles
139
+
140
+ Use `auth=` for one or more named authentication strategies and `role=` for
141
+ route-level role authorization:
142
+
143
+ ```text
144
+ GET /account Account#show auth=session
145
+ GET /admin Admin::Dashboard auth=session role=admin
146
+ GET /editorial Editorial#show auth=session role=admin,editor
147
+ GET /api/data Api#show auth=session,api_key response=json
148
+ ```
149
+
150
+ `auth=session,api_key` is OR logic: strategies run left to right, an
151
+ authenticated success stops the chain, and a later strategy can be tried after
152
+ a plain failure. `role=admin,editor` also uses OR logic: any listed role is
153
+ enough. See the [authentication guide](../guides/authentication.md).
154
+
155
+ Authentication and role options are security-gating options. They must use
156
+ lowercase `auth=`, `role=`, or `csrf=` with a non-empty value. Otto raises
157
+ `Otto::RouteDefinitionError` instead of silently treating a malformed token as
158
+ an unprotected route:
159
+
160
+ ```text
161
+ # Rejected during route definition parsing:
162
+ GET /admin Admin#show auth
163
+ GET /admin Admin#show role=
164
+ POST /submit Form#save CSRF=exempt
165
+ ```
166
+
167
+ ## Configuration timing
168
+
169
+ Routes are loaded during `Otto.new`. Configuration remains available for
170
+ multi-step setup, but Otto freezes configuration on the first request in normal
171
+ operation. Register authentication strategies, lambda handlers, middleware,
172
+ helpers, and security settings before serving traffic.
173
+
174
+ ## Related contracts
175
+
176
+ - [Routing guide](../guides/routing.md) — application-oriented examples.
177
+ - [Authentication guide](../guides/authentication.md) — strategy and
178
+ authorization behavior.
179
+ - [Configuration and lifecycle code](../../lib/otto/core/configuration.rb) and
180
+ [`RouteDefinition`](../../lib/otto/route_definition.rb) — implementation
181
+ source for the exact contract.
@@ -0,0 +1,86 @@
1
+ # Runtime and dependency security policy
2
+
3
+ This policy explains what Otto's Ruby and gem version declarations guarantee,
4
+ what they do not guarantee, and what applications using Otto must do to keep a
5
+ deployed bundle patched.
6
+
7
+ ## Ruby compatibility and security maintenance
8
+
9
+ Otto separates **runtime compatibility** from **interpreter security
10
+ maintenance**. A blocking compatibility target must pass Otto's test suite;
11
+ provisional targets are exercised without blocking releases. Only the Ruby
12
+ project can provide security maintenance for the interpreter.
13
+
14
+ | Ruby version | Otto compatibility policy | CI status |
15
+ | --- | --- | --- |
16
+ | 3.2 | Retained as a compatibility target for now, despite upstream end of life | Blocking, with locked and freshly resolved dependencies |
17
+ | 3.3–3.4 | Supported compatibility targets | Blocking, with locked and freshly resolved dependencies |
18
+ | 3.5–4.0 | Accepted by the gem's Ruby version range, but compatibility remains provisional | Experimental and non-blocking, with locked and freshly resolved dependencies |
19
+
20
+ Ruby 3.2 reached upstream end of life on April 1, 2026. It receives no
21
+ interpreter security maintenance. Otto's continued compatibility testing cannot
22
+ correct vulnerabilities in Ruby 3.2 itself. Applications with a security
23
+ maintenance requirement should run Otto on a Ruby version that the Ruby project
24
+ currently maintains. Check the [Ruby branch maintenance
25
+ status](https://www.ruby-lang.org/en/downloads/branches/) when selecting a
26
+ runtime.
27
+
28
+ The `required_ruby_version` range in `otto.gemspec` controls whether RubyGems
29
+ may install Otto. It does not mean every version in that range has the same CI
30
+ or upstream security status. The table above is the support policy for the
31
+ current matrix.
32
+
33
+ ## What dependency ranges mean
34
+
35
+ The runtime dependency ranges in `otto.gemspec` express the versions Otto
36
+ expects to be API-compatible with. Their lower bounds are compatibility
37
+ baselines, not a promise that every allowed version is free of known
38
+ vulnerabilities or still receives security fixes.
39
+
40
+ Otto may exclude a known-bad dependency release or raise a lower bound when a
41
+ vulnerability affects Otto users. Such a constraint is a targeted response, not
42
+ a substitute for auditing the complete resolved dependency graph. A future
43
+ advisory can make any previously acceptable version unsafe while it still
44
+ satisfies the gemspec.
45
+
46
+ Otto's committed `Gemfile.lock` makes maintainer development and CI resolution
47
+ reproducible. Bundler does not use that lockfile when Otto is installed as a
48
+ dependency of another application. Otto's locked and freshly resolved CI jobs
49
+ test compatibility; neither job selects or certifies a secure dependency set
50
+ for consumer applications.
51
+
52
+ ## Consumer lockfile policy
53
+
54
+ The deployable application is responsible for the final dependency graph. An
55
+ application using Otto should:
56
+
57
+ 1. Commit its `Gemfile.lock` and deploy that exact resolution.
58
+ 2. Make [`bundler-audit`](https://github.com/rubysec/bundler-audit) available
59
+ in application CI, then run it with an updated advisory database on every
60
+ lockfile change, on a scheduled weekly job, and before each production
61
+ deployment:
62
+
63
+ ```sh
64
+ bundle-audit check --update
65
+ ```
66
+
67
+ 3. Treat a relevant advisory as a release blocker. Update the affected gem and
68
+ re-run the application's tests and audit. For a conservative targeted
69
+ update, replace `GEM_NAME` with the affected gem:
70
+
71
+ ```sh
72
+ bundle update --conservative GEM_NAME
73
+ bundle-audit check --update
74
+ ```
75
+
76
+ 4. Enable an automated dependency updater, or review `bundle outdated`
77
+ manually at least weekly, so patched releases reach the application
78
+ lockfile.
79
+ 5. Audit the Ruby interpreter, operating system packages, and native libraries
80
+ separately. `bundler-audit` checks Ruby gems; it does not make an
81
+ end-of-life Ruby secure.
82
+
83
+ If the patched dependency version falls outside Otto's declared range,
84
+ applications should upgrade Otto when a compatible release is available and
85
+ report the blocked update to the maintainers. Do not assume that a successful
86
+ `bundle install` means the resolved bundle has no known vulnerabilities.
@@ -20,50 +20,54 @@ The example is organized to separate concerns:
20
20
  - `app.rb`: Loader that requires all controller and logic files
21
21
  - `app/controllers/`: Handler classes (`RoutesApp`, namespaced controllers)
22
22
  - `app/logic/`: Business logic classes (simple, nested, namespaced)
23
- - `run.rb`, `puma.rb`, `test.rb`: Alternative server/test runners
23
+
24
24
 
25
25
  ## Key Features Demonstrated
26
26
 
27
27
  ### Response Types
28
28
  Define how responses are formatted directly in routes:
29
29
  ```
30
- GET /api/users UserController#list response=json
31
- GET /page PageController#show response=view
32
- GET /old-url PageController#new-url response=redirect
30
+ GET /api/users RoutesApp#list_users response=json
31
+ GET /dashboard RoutesApp#dashboard response=view
32
+ GET /login RoutesApp#login_redirect response=redirect
33
33
  ```
34
34
 
35
35
  ### Logic Classes
36
36
  Route to specialized classes that encapsulate business logic:
37
37
  ```
38
- GET /calculate DataProcessor # Otto auto-instantiates and calls #process
39
- GET /report ReportGenerator # Same pattern
38
+ GET /logic/simple SimpleLogic
39
+ GET /logic/data DataLogic response=json
40
40
  ```
41
41
 
42
+ The classes in `app/logic/` expose the logic used by these routes.
43
+
42
44
  ### CSRF Exemption
43
45
  Mark routes that don't need CSRF tokens (APIs, webhooks):
44
46
  ```
45
- POST /api/webhook WebhookHandler#receive csrf=exempt
47
+ POST /api/webhook RoutesApp#webhook_handler csrf=exempt
46
48
  ```
47
49
 
48
50
  ### Namespaced Routing
49
51
  Handle complex class hierarchies naturally:
50
52
  ```
51
- GET /v2/dashboard V2::Logic::Dashboard
52
- GET /admin/panel Admin::Panel#dashboard
53
+ GET /logic/v2/dashboard V2::Logic::Dashboard response=view
54
+ GET /logic/admin Admin::Panel
53
55
  ```
54
56
 
55
57
  ### Custom Parameters
56
58
  Add arbitrary key-value pairs for flexible routing:
57
59
  ```
58
- GET /admin AdminPanel#dashboard role=admin
60
+ GET /feature/flags RoutesApp#feature_flags feature=advanced mode=enabled
59
61
  ```
60
62
 
63
+ These are route configuration values, not query-string parameters.
64
+
61
65
  ### Lambda / Inline Route Handlers (Issue #41)
62
66
  Route to a proc that you **pre-register** by name, using the `&` prefix:
63
67
  ```
64
68
  GET /ping &health_check response=json
65
- POST /webhook &receive_webhook response=json csrf=exempt
66
- GET /go &to_dashboard response=redirect
69
+ POST /hooks/receive &receive_webhook response=json csrf=exempt
70
+ GET /go/dashboard &to_dashboard response=redirect
67
71
  ```
68
72
 
69
73
  The `&name` token is a plain string key looked up (O(1)) in a registry you
@@ -104,67 +108,49 @@ loading**. A route naming an unregistered handler fails with a clear
104
108
  instead of executing anything. The registered procs are, of course, trusted
105
109
  code that you wrote.
106
110
 
107
- Note: `csrf=exempt` is parse-and-expose parity with controller routes the CSRF
108
- middleware does not enforce it for any handler kind.
111
+ This example enables CSRF protection in `config.rb`; the `routes` file marks its API and webhook examples with `csrf=exempt`.
112
+
113
+ ## Run it
109
114
 
110
- ## How to Run
115
+ ### Prerequisites
111
116
 
112
- ### Using rackup (recommended)
117
+ Run this example from an Otto source checkout with Ruby 3.2 through 4.0 and Bundler. `rackup` is a development dependency in the root `Gemfile`, so enable that optional group before installing the bundle.
113
118
 
114
119
  ```sh
120
+ git clone https://github.com/delano/otto.git
121
+ cd otto
122
+ bundle config set with development
123
+ bundle install
115
124
  cd examples/advanced_routes
116
- rackup config.ru
125
+ bundle exec rackup config.ru
117
126
  ```
118
127
 
119
- ### Using alternative runners
120
-
121
- ```sh
122
- ruby run.rb # Basic rackup
123
- ruby puma.rb # Using puma server
124
- ruby test.rb # For testing
125
- ```
128
+ The server listens on `http://localhost:9292`.
126
129
 
127
- The application will be running at `http://localhost:9292`.
130
+ > **Current limitation:** This checked-in example does not finish booting: `app/logic/complex/business/handler.rb` attempts to declare `Complex` as a module, which conflicts with Ruby's built-in `Complex` class. Until that source issue is fixed, `rackup` exits before listening and the checks below cannot be run. They document the routes and expected responses configured by this example.
128
131
 
129
- ## Testing Routes
132
+ ### Verify routes
130
133
 
131
- Use curl to test the different routes:
134
+ In another terminal, run these checks against routes defined in `routes`:
132
135
 
133
136
  ```sh
134
137
  # JSON response
135
- curl http://localhost:9292/json/test
138
+ curl -i http://localhost:9292/api/health
136
139
 
137
- # View response
138
- curl http://localhost:9292/view/test
140
+ # HTML view response
141
+ curl -i http://localhost:9292/dashboard
139
142
 
140
- # Logic class routing
141
- curl http://localhost:9292/logic/simple
143
+ # Registered lambda handler
144
+ curl -i http://localhost:9292/ping
142
145
 
143
- # Namespaced routing
144
- curl http://localhost:9292/logic/v2/dashboard
146
+ # Redirect response; inspect the Location header
147
+ curl -i http://localhost:9292/go/dashboard
145
148
 
146
- # Custom parameters
147
- curl "http://localhost:9292/custom?role=admin"
149
+ # Route configuration values
150
+ curl -i http://localhost:9292/feature/flags
148
151
  ```
149
152
 
150
- ## Expected Output
151
-
152
- ```
153
- Listening on 127.0.0.1:9292, CTRL+C to stop
154
-
155
- [JSON response]
156
- GET /json/test 200 OK
157
- Content-Type: application/json
158
- {"status": "success", "data": {...}}
159
-
160
- [Logic class routing]
161
- GET /logic/simple 200 OK
162
- {"processed": true, "input": "test"}
163
-
164
- [Namespaced routing]
165
- GET /logic/v2/dashboard 200 OK
166
- {"version": "2.0", "dashboard": {...}}
167
- ```
153
+ Expect `200` responses for the first three and last commands. `/api/health` returns JSON containing `healthy`, `/dashboard` returns HTML containing `Dashboard`, `/ping` returns JSON containing `ok`, and `/feature/flags` returns JSON containing `advanced`. `/go/dashboard` returns `302` with `Location: /dashboard`.
168
154
 
169
155
  ## File Structure Details
170
156
 
@@ -191,9 +177,9 @@ The `routes` file is extensively commented to explain each feature:
191
177
  - Review the `routes` file for syntax reference
192
178
  - Examine handler methods to see request/response patterns
193
179
  - Check logic classes for business logic encapsulation patterns
194
- - Explore [Authentication](../authentication_strategies/) for protecting routes
195
- - See [Security Features](../security_features/) for CSRF, validation, file uploads
180
+ - Explore [Authentication](../authentication_strategies/README.md) for protecting routes
181
+ - See [Security Features](../security_features/README.md) for CSRF, validation, and file uploads
196
182
 
197
183
  ## Further Reading
198
184
 
199
- - [CLAUDE.md](../../CLAUDE.md) - Comprehensive developer guidance
185
+ - [Project README](../../README.md) - Installation and framework overview