otto 2.0.0.pre2 → 2.0.0.pre7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -3
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +116 -45
  10. data/Gemfile +5 -2
  11. data/Gemfile.lock +70 -24
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/ipaddr-encoding-quirk.md +34 -0
  17. data/docs/migrating/v2.0.0-pre2.md +11 -18
  18. data/examples/advanced_routes/README.md +137 -20
  19. data/examples/authentication_strategies/README.md +212 -19
  20. data/examples/authentication_strategies/config.ru +0 -1
  21. data/examples/backtrace_sanitization_demo.rb +86 -0
  22. data/examples/basic/README.md +61 -10
  23. data/examples/error_handler_registration.rb +136 -0
  24. data/examples/logging_improvements.rb +76 -0
  25. data/examples/mcp_demo/README.md +187 -27
  26. data/examples/security_features/README.md +249 -30
  27. data/examples/simple_geo_resolver.rb +107 -0
  28. data/lib/otto/core/configuration.rb +90 -45
  29. data/lib/otto/core/error_handler.rb +138 -8
  30. data/lib/otto/core/file_safety.rb +2 -2
  31. data/lib/otto/core/freezable.rb +93 -0
  32. data/lib/otto/core/middleware_stack.rb +25 -18
  33. data/lib/otto/core/router.rb +62 -9
  34. data/lib/otto/core/uri_generator.rb +2 -2
  35. data/lib/otto/core.rb +10 -0
  36. data/lib/otto/design_system.rb +2 -2
  37. data/lib/otto/env_keys.rb +65 -12
  38. data/lib/otto/helpers/base.rb +2 -2
  39. data/lib/otto/helpers/request.rb +85 -2
  40. data/lib/otto/helpers/response.rb +5 -5
  41. data/lib/otto/helpers/validation.rb +2 -2
  42. data/lib/otto/helpers.rb +6 -0
  43. data/lib/otto/locale/config.rb +56 -0
  44. data/lib/otto/locale/middleware.rb +160 -0
  45. data/lib/otto/locale.rb +10 -0
  46. data/lib/otto/logging_helpers.rb +273 -0
  47. data/lib/otto/mcp/auth/token.rb +2 -2
  48. data/lib/otto/mcp/protocol.rb +2 -2
  49. data/lib/otto/mcp/rate_limiting.rb +2 -2
  50. data/lib/otto/mcp/registry.rb +2 -2
  51. data/lib/otto/mcp/route_parser.rb +2 -2
  52. data/lib/otto/mcp/schema_validation.rb +2 -2
  53. data/lib/otto/mcp/server.rb +2 -2
  54. data/lib/otto/mcp.rb +5 -0
  55. data/lib/otto/privacy/config.rb +201 -0
  56. data/lib/otto/privacy/geo_resolver.rb +285 -0
  57. data/lib/otto/privacy/ip_privacy.rb +177 -0
  58. data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
  59. data/lib/otto/privacy.rb +31 -0
  60. data/lib/otto/response_handlers/auto.rb +2 -0
  61. data/lib/otto/response_handlers/base.rb +2 -0
  62. data/lib/otto/response_handlers/default.rb +2 -0
  63. data/lib/otto/response_handlers/factory.rb +2 -0
  64. data/lib/otto/response_handlers/json.rb +2 -0
  65. data/lib/otto/response_handlers/redirect.rb +2 -0
  66. data/lib/otto/response_handlers/view.rb +2 -0
  67. data/lib/otto/response_handlers.rb +2 -2
  68. data/lib/otto/route.rb +4 -4
  69. data/lib/otto/route_definition.rb +42 -15
  70. data/lib/otto/route_handlers/base.rb +2 -1
  71. data/lib/otto/route_handlers/class_method.rb +18 -25
  72. data/lib/otto/route_handlers/factory.rb +18 -16
  73. data/lib/otto/route_handlers/instance_method.rb +8 -5
  74. data/lib/otto/route_handlers/lambda.rb +8 -20
  75. data/lib/otto/route_handlers/logic_class.rb +25 -8
  76. data/lib/otto/route_handlers.rb +2 -2
  77. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
  78. data/lib/otto/security/authentication/auth_strategy.rb +13 -6
  79. data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
  80. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
  82. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  83. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  84. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  85. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  86. data/lib/otto/security/authentication.rb +5 -6
  87. data/lib/otto/security/authorization_error.rb +73 -0
  88. data/lib/otto/security/config.rb +53 -9
  89. data/lib/otto/security/configurator.rb +17 -15
  90. data/lib/otto/security/csrf.rb +2 -2
  91. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  92. data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
  93. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  94. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  95. data/lib/otto/security/rate_limiter.rb +2 -2
  96. data/lib/otto/security/rate_limiting.rb +2 -2
  97. data/lib/otto/security/validator.rb +2 -2
  98. data/lib/otto/security.rb +12 -0
  99. data/lib/otto/static.rb +2 -2
  100. data/lib/otto/utils.rb +27 -2
  101. data/lib/otto/version.rb +3 -3
  102. data/lib/otto.rb +344 -89
  103. data/otto.gemspec +9 -2
  104. metadata +72 -8
  105. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6215d43a8ce52d332bc046b6d51e8474c243e804bb146381fcd617fa8aebd1f
4
- data.tar.gz: 7d73de1613bdd0f2450c2b3e16c8ef0b7020ce417f38b506c70c9b05d414d561
3
+ metadata.gz: 69a46d80f1fcc3f2472c44554fd7102645226e264befdd23af9871ad4f5fafaf
4
+ data.tar.gz: 54d8b433d49b549e17d3406aa011b6f4f80c50eefc5a6fa51dde1772fde8b8f2
5
5
  SHA512:
6
- metadata.gz: 4f04484aadab6fd972ecc261ed7da8291996e1623ae5df198b08550f82afa86e7d651beb7448d75b690b7505ae5d4d8443dd24c2a98bec7bb4621bcbb8b23950
7
- data.tar.gz: bdfe655d0f597f92bfacaf89342aa0d026ecf2e89d1975ebc1835b2521a38f3bec8ba57cce94992ab0ac2eae4f01ce0080f849f82d08bb15e3473c8794fa9667
6
+ metadata.gz: b518140043ad7ab983fb99e85ac9643ca4527385930d77a4b01e2b241b7212854fbc5e8875bc58b17e3d17c66fcfc6455b85587d023ceec13006876bec79a0d9
7
+ data.tar.gz: f64b842b58acc746ad39d58eac0fb57c245f1db29755f89d259d64bd127db3043034a9e8ada0ee19c04d969ec1bb34b515f5dfc1fcf36c80ceaeb9123ac2de6e
@@ -28,8 +28,6 @@ jobs:
28
28
  fail-fast: false
29
29
  matrix:
30
30
  include:
31
- - ruby: "3.2"
32
- experimental: false
33
31
  - ruby: "3.3"
34
32
  experimental: false
35
33
  - ruby: "3.4"
@@ -47,7 +45,7 @@ jobs:
47
45
  bundler-cache: ${{ !matrix.experimental }}
48
46
 
49
47
  - name: Setup tmate session
50
- uses: mxschmitt/action-tmate@7b6a61a73bbb9793cb80ad69b8dd8ac19261834c # v3
48
+ uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3
51
49
  if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
52
50
  with:
53
51
  detached: true
@@ -2,13 +2,8 @@ name: Claude Code Review
2
2
 
3
3
  on:
4
4
  pull_request:
5
- types: [opened, synchronize]
6
- # Optional: Only run on specific file changes
7
- # paths:
8
- # - "src/**/*.ts"
9
- # - "src/**/*.tsx"
10
- # - "src/**/*.js"
11
- # - "src/**/*.jsx"
5
+ types: [opened, synchronize, labeled]
6
+ workflow_dispatch:
12
7
 
13
8
  jobs:
14
9
  claude-review:
@@ -19,6 +14,11 @@ jobs:
19
14
  # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
15
 
21
16
  runs-on: ubuntu-latest
17
+ if: |
18
+ (github.event.action == 'opened') ||
19
+ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
20
+ (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'claude-review'))
21
+
22
22
  permissions:
23
23
  contents: read
24
24
  pull-requests: read
@@ -33,10 +33,12 @@ jobs:
33
33
 
34
34
  - name: Run Claude Code Review
35
35
  id: claude-review
36
- uses: anthropics/claude-code-action@v1
36
+ uses: anthropics/claude-code-action@beta
37
37
  with:
38
38
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
- prompt: |
39
+
40
+ # Direct prompt for automated review (no @claude mention needed)
41
+ direct_prompt: |
40
42
  Please review this pull request and provide feedback on:
41
43
  - Code quality and best practices
42
44
  - Potential bugs or issues
@@ -46,8 +48,22 @@ jobs:
46
48
 
47
49
  Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
48
50
 
49
- Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
51
+ # Use sticky comments to reuse the same comment on subsequent pushes to the same PR
52
+ use_sticky_comment: true
50
53
 
51
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
52
- # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
53
- claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
54
+ - name: Remove claude-review label
55
+ # Remove label whether success or failure - prevents getting stuck
56
+ if: always() && github.event.action != 'opened'
57
+ uses: actions/github-script@v8
58
+ with:
59
+ script: |
60
+ try {
61
+ await github.rest.issues.removeLabel({
62
+ owner: context.repo.owner,
63
+ repo: context.repo.repo,
64
+ issue_number: context.issue.number,
65
+ name: 'claude-review'
66
+ });
67
+ } catch (error) {
68
+ console.log('Label not found or already removed');
69
+ }
@@ -0,0 +1,146 @@
1
+ # .github/workflows/code-smells.yml
2
+ ---
3
+ name: Code Smells
4
+
5
+ on:
6
+ pull_request:
7
+ branches: [main]
8
+ push:
9
+ branches: [main]
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write # Needed to post comments on PRs
15
+
16
+ jobs:
17
+ reek-analysis:
18
+ name: Reek Code Analysis
19
+ runs-on: ubuntu-24.04
20
+ timeout-minutes: 5
21
+
22
+ steps:
23
+ - name: Checkout code
24
+ uses: actions/checkout@v5
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: 3.4
30
+ bundler-cache: true
31
+
32
+ - name: Configure Bundler for secure gem installation
33
+ run: |
34
+ bundle config set --local path 'vendor/bundle'
35
+ bundle config set --local deployment 'false'
36
+
37
+ - name: Install dependencies
38
+ run: bundle install --jobs 4 --retry 3
39
+
40
+ - name: Run Reek analysis
41
+ run: |
42
+ echo "=== Running Reek code analysis ==="
43
+ echo "This analysis identifies code smells and potential improvements."
44
+ echo "Results are informational and won't fail the build."
45
+ echo ""
46
+
47
+ # Run reek and capture output (don't fail on warnings)
48
+ # Use success-exit-code to prevent failures from stopping the analysis
49
+ bundle exec reek --format=text --success-exit-code 0 --failure-exit-code 0 || true
50
+
51
+ echo ""
52
+ echo "=== Reek analysis complete ==="
53
+ continue-on-error: true # Don't fail the build on code smells
54
+
55
+ - name: Generate Reek report
56
+ run: |
57
+ echo "=== Generating detailed Reek report ==="
58
+
59
+ # Generate JSON report for potential future processing
60
+ bundle exec reek --format=json --success-exit-code 0 --failure-exit-code 0 > reek-report.json || true
61
+
62
+ # If no JSON was generated, create an empty valid JSON array
63
+ if [ ! -s reek-report.json ]; then
64
+ echo "[]" > reek-report.json
65
+ echo "No code smells detected - created empty report"
66
+ else
67
+ echo "Reek JSON report generated: $(wc -l < reek-report.json) lines"
68
+ echo "Top code smell types found:"
69
+ jq -r '.[].smells[].smell_type' reek-report.json 2>/dev/null | sort | uniq -c | sort -rn | head -10 || echo "Unable to parse JSON report"
70
+ fi
71
+
72
+ # Also generate a human-readable HTML report for easier viewing
73
+ bundle exec reek --format=html --success-exit-code 0 --failure-exit-code 0 > reek-report.html || echo "<!-- No code smells detected -->" > reek-report.html
74
+ continue-on-error: true
75
+
76
+ - name: Upload Reek report as artifact
77
+ uses: actions/upload-artifact@v5
78
+ if: always()
79
+ with:
80
+ name: reek-report
81
+ path: |
82
+ reek-report.json
83
+ reek-report.html
84
+ if-no-files-found: ignore
85
+ retention-days: 30
86
+
87
+ additional-checks:
88
+ name: Additional Quality Checks
89
+ runs-on: ubuntu-24.04
90
+ timeout-minutes: 5
91
+
92
+ steps:
93
+ - name: Checkout code
94
+ uses: actions/checkout@v5
95
+
96
+ - name: Set up Ruby
97
+ uses: ruby/setup-ruby@v1
98
+ with:
99
+ ruby-version: 3.4
100
+ bundler-cache: true
101
+
102
+ - name: Configure Bundler for secure gem installation
103
+ run: |
104
+ bundle config set --local path 'vendor/bundle'
105
+ bundle config set --local deployment 'false'
106
+
107
+ - name: Install dependencies
108
+ run: bundle install --jobs 4 --retry 3
109
+
110
+ - name: Check for TODO/FIXME comments
111
+ run: |
112
+ echo "=== Scanning for TODO/FIXME comments ==="
113
+ echo "This helps track technical debt and action items."
114
+ echo ""
115
+
116
+ # Find TODO/FIXME comments (excluding vendor and tmp directories)
117
+ find . -name "*.rb" -not -path "./vendor/*" -not -path "./tmp/*" | \
118
+ xargs grep -Hn -i -E "(TODO|FIXME|HACK|XXX|NOTE):" 2>/dev/null | \
119
+ head -20 || echo "No TODO/FIXME comments found"
120
+ continue-on-error: true
121
+
122
+ - name: Check Ruby file syntax
123
+ run: |
124
+ echo "=== Checking Ruby syntax ==="
125
+ echo "Validates that all Ruby files have correct syntax."
126
+ echo ""
127
+
128
+ find . -name "*.rb" -not -path "./vendor/*" -not -path "./tmp/*" | \
129
+ while read -r file; do
130
+ if ! ruby -c "$file" > /dev/null 2>&1; then
131
+ echo "Syntax error in: $file"
132
+ ruby -c "$file"
133
+ fi
134
+ done
135
+ continue-on-error: true
136
+
137
+ - name: Check for long lines
138
+ run: |
139
+ echo "=== Checking for long lines (>120 characters) ==="
140
+ echo "Identifies potentially hard-to-read code lines."
141
+ echo ""
142
+
143
+ find . -name "*.rb" -not -path "./vendor/*" -not -path "./tmp/*" | \
144
+ xargs grep -Hn "^.\{121,\}$" | \
145
+ head -10 || echo "No overly long lines found"
146
+ continue-on-error: true
data/.gitignore CHANGED
@@ -11,8 +11,12 @@
11
11
  .*.json
12
12
  !LICENSE.txt
13
13
  !spec/fixtures/*.txt
14
+ !examples/**/*.md
15
+ !README.md
16
+ !CLAUDE.md
14
17
  .ruby-version
15
18
  appendonlydir
19
+ data/
16
20
  etc/config
17
21
  log
18
22
  tmp
@@ -96,12 +96,12 @@ repos:
96
96
 
97
97
  # Commit message issue tracking integration
98
98
  - repo: https://github.com/avilaton/add-msg-issue-prefix-hook
99
- rev: v0.0.12
99
+ rev: v0.0.13
100
100
  hooks:
101
101
  - id: add-msg-issue-prefix
102
102
  stages: [prepare-commit-msg]
103
103
  description: Automatically prefix commits with issue numbers
104
104
  args:
105
105
  - "--default="
106
- - '--pattern=(?:i18n(?=\/)|[a-zA-Z0-9]{0,10}-?[0-9]{1,5})'
106
+ - "--pattern=(i18n(?=/)|([a-zA-Z0-9]{0,10}-?[0-9]{1,5}))"
107
107
  - "--template=[#{}]"
data/.reek.yml ADDED
@@ -0,0 +1,99 @@
1
+ # .reek.yml
2
+ #
3
+ # Reek configuration for Otto
4
+ #
5
+ # Basic commands:
6
+ # bundle exec reek # Analyze all Ruby files
7
+ # bundle exec reek lib/ # Analyze specific directory
8
+ # bundle exec reek lib/otto.rb # Analyze specific file
9
+ # bundle exec reek --help # Show all options
10
+ # bundle exec reek --docs # Open documentation
11
+ #
12
+ # Advanced usage:
13
+ # bundle exec reek --format=html > report.html # Generate HTML report
14
+ # bundle exec reek --format=json # JSON output for CI
15
+ # bundle exec reek --config .reek.yml # Use specific config
16
+ # bundle exec reek --show-docs IrresponsibleModule # Explain specific smell
17
+ # bundle exec reek --failure-exit-code 1 # Exit with error on smells (for CI)
18
+
19
+ ---
20
+ detectors:
21
+ # Disable some detectors for initial adoption
22
+ # You can gradually enable these as you clean up the codebase
23
+
24
+ # Class/Module Structure
25
+ IrresponsibleModule:
26
+ enabled: false # Modules without documentation - start with this disabled
27
+
28
+ # Method Complexity
29
+ TooManyStatements:
30
+ enabled: true
31
+ max_statements: 15 # Default is 5, relaxed for initial adoption
32
+
33
+ TooManyMethods:
34
+ enabled: true
35
+ max_methods: 25 # Default is 15, relaxed for ORMs which often have many methods
36
+
37
+ LongParameterList:
38
+ enabled: true
39
+ max_params: 4 # Default is 3, slightly relaxed
40
+
41
+ # Data Classes and Feature Envy
42
+ DataClump:
43
+ enabled: true
44
+
45
+ FeatureEnvy:
46
+ enabled: true
47
+
48
+ # Control Structure
49
+ NestedIterators:
50
+ enabled: true
51
+ max_allowed_nesting: 2 # Default is 1, relaxed for data processing
52
+
53
+ # Variable and Constant Usage
54
+ UnusedParameters:
55
+ enabled: true
56
+
57
+ InstanceVariableAssumption:
58
+ enabled: true
59
+
60
+ # Naming
61
+ UncommunicativeParameterName:
62
+ enabled: true
63
+ reject:
64
+ - "/^.$/" # Single letter names
65
+ - "/[0-9]$/" # Names ending in numbers
66
+ - "/^_/" # Names starting with underscore (common Ruby pattern)
67
+ accept: []
68
+
69
+ UncommunicativeVariableName:
70
+ enabled: true
71
+ reject:
72
+ - "/^.$/" # Single letter names
73
+ - "/[0-9]$/" # Names ending in numbers
74
+ accept:
75
+ - e # Exception variable
76
+ - id # Common identifier
77
+ - db # Database connection
78
+ - op # Operation
79
+ - io # Input/output
80
+
81
+ UncommunicativeMethodName:
82
+ enabled: true
83
+ reject:
84
+ - "/^.$/" # Single letter method names
85
+ - "/[0-9]$/" # Methods ending in numbers
86
+ accept:
87
+ - "<<" # Common Ruby operator overload
88
+
89
+ # Directory and file exclusions
90
+ exclude_paths:
91
+ - "vendor/**/*.rb"
92
+ - "tmp/**/*.rb"
93
+ - "try/**/*.rb" # Test files using tryouts framework
94
+ - "examples/**/*.rb" # Example code files
95
+ - "bin/*" # Executable scripts
96
+ - "*.gemspec" # Gem specification files
97
+
98
+ # Note: For limiting warnings output, use CLI: bundle exec reek | head -50
99
+ # Note: For failure exit codes, use CLI: bundle exec reek --failure-exit-code 1
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,95 @@ 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.0.0.pre6:
11
+
12
+ 2.0.0.pre6 — TBD
13
+ ================
14
+
15
+ Changed
16
+ -------
17
+
18
+ - **BREAKING**: ``Otto.on_request_complete`` is now an instance method instead of a class method. This fixes duplicate callback invocations in multi-app architectures (e.g., Rack::URLMap with multiple Otto instances). Each Otto instance now maintains its own isolated set of callbacks that only fire for requests processed by that specific instance.
19
+
20
+ **Migration**: Change ``Otto.on_request_complete { |req, res, dur| ... }`` to ``otto.on_request_complete { |req, res, dur| ... }``
21
+
22
+ - **Logging**: Eliminated duplicate error logging in route handlers. Previously, errors produced two log lines ("Handler execution failed" + "Unhandled error in request"). Now produces a single comprehensive error log with all context (handler, duration, error_id). Lambda handlers now use centralized error handling for consistency. #86
23
+
24
+ Fixed
25
+ -----
26
+
27
+ - Fixed issue #84 where ``on_request_complete`` callbacks would fire N times per request in multi-app architectures, causing duplicate logging and metrics
28
+ - Fixed ``Otto.structured_log`` to respect ``Otto.debug`` flag - debug logs are now properly skipped when ``Otto.debug = false``
29
+
30
+ AI Assistance
31
+ -------------
32
+
33
+ - This enhancement was developed with assistance from Claude Code (Opus 4.1)
34
+
35
+ .. _changelog-2.0.0.pre5:
36
+
37
+ 2.0.0.pre5 — 2025-10-21
38
+ =======================
39
+
40
+ Added
41
+ -----
42
+
43
+ - Added ``Otto::LoggingHelpers.log_timed_operation`` for automatic timing and error handling of operations
44
+ - Added ``Otto::LoggingHelpers.log_backtrace`` for consistent backtrace logging with correlation fields
45
+ - Added microsecond-precision timing to configuration freeze process
46
+ - Added unique error ID generation for nested error handler failures (links via ``original_error_id``)
47
+
48
+ Changed
49
+ -------
50
+
51
+ - Timing precision standardization: All timing calculations now use microsecond precision instead of milliseconds. This affects authentication duration tracking and request lifecycle timing. Duration values are now reported in microseconds as integers (e.g., ``15200`` instead of ``15.2``).
52
+ - Request completion hooks API improvement: ``Otto.on_request_complete`` callbacks now receive a ``Rack::Response`` object instead of the raw ``[status, headers, body]`` tuple. This provides a more developer-friendly API consistent with ``Rack::Request``, allowing clean access via ``res.status``, ``res.headers``, and ``res.body`` instead of array indexing.
53
+ - All timing now uses microseconds (``Otto::Utils.now_in_μs``) for consistency
54
+ - Configuration freeze process now logs detailed timing metrics
55
+
56
+ Documentation
57
+ -------------
58
+
59
+ - Added example application demonstrating three new logging patterns (``examples/logging_improvements.rb``)
60
+ - Documented base context pattern for downstream projects to inject custom correlation fields
61
+ - Added output examples for both structured and standard loggers
62
+
63
+ AI Assistance
64
+ -------------
65
+
66
+ - This enhancement was developed with assistance from Claude Code (Opus 4.1)
67
+
68
+ .. _changelog-2.0.0.pre4:
69
+
70
+
71
+ 2.0.0.pre4 — 2025-10-20
72
+ =======================
73
+ Changed
74
+ -------
75
+ - Authentication moved from middleware to RouteAuthWrapper at handler level (executes after routing)
76
+ - RouteAuthWrapper now wraps all routes and provides session persistence, security headers, strategy caching, and pattern matching (exact, prefix, fallback)
77
+ - env['otto.strategy_result'] now guaranteed present on all routes (authenticated or anonymous)
78
+ - Renamed MiddlewareStack#build_app to #wrap (reflects per-request wrapping vs one-time initialization)
79
+
80
+ Removed
81
+ -------
82
+ - AuthenticationMiddleware (executed before routing)
83
+ - enable_authentication! (RouteAuthWrapper handles auth automatically)
84
+ - Defensive nil fallback from LogicClassHandler (no longer needed)
85
+
86
+ Fixed
87
+ -----
88
+ - Session persistence: env['rack.session'] now references same object as strategy_result.session
89
+ - Security headers included on all auth failure responses (401/302)
90
+ - Anonymous routes now receive StrategyResult with IP metadata
91
+
92
+ Documentation
93
+ -------------
94
+ - Updated CLAUDE.md with RouteAuthWrapper architecture
95
+ - Updated env_keys.rb to document strategy_result guarantee
96
+ - Added tests for anonymous route handling
97
+
98
+
10
99
  .. _changelog-2.0.0.pre2:
11
100
 
12
101
  2.0.0.pre2 — 2025-10-11
@@ -60,6 +149,7 @@ AI Assistance
60
149
  - Comprehensive migration of Logic classes and documentation with AI guidance for consistency
61
150
  - Automated test validation and intelligent file organization following Ruby conventions
62
151
 
152
+
63
153
  .. _changelog-2.0.0-pre1:
64
154
 
65
155
  2.0.0-pre1 — 2025-09-10
data/CLAUDE.md CHANGED
@@ -1,56 +1,127 @@
1
1
  # CLAUDE.md
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3
+ This file provides essential guidance to Claude Code when working with Otto.
4
+
5
+ ## Error Handler Registration
6
+
7
+ Register handlers for expected business logic errors to avoid logging them as 500 errors:
8
+
9
+ ```ruby
10
+ otto = Otto.new('routes.txt')
11
+ otto.register_error_handler(YourApp::NotFound, status: 404, log_level: :info)
12
+ otto.register_error_handler(YourApp::RateLimited, status: 429, log_level: :warn)
13
+ ```
14
+
15
+ Must be registered before first request (before configuration freezing).
16
+
17
+ ## Authentication Architecture
18
+
19
+ Authentication is handled by `RouteAuthWrapper` at the handler level, NOT by middleware.
20
+
21
+ ### Basic Configuration
22
+
23
+ ```ruby
24
+ otto.add_auth_strategy('session', SessionStrategy.new)
25
+ otto.add_auth_strategy('apikey', APIKeyStrategy.new)
26
+ ```
27
+
28
+ - Strategy names must be unique
29
+ - Routes with `auth` requirements are automatically wrapped
30
+ - Must be configured before first request
31
+
32
+ ### Multi-Strategy Authentication
33
+
34
+ Routes support multiple strategies with OR logic:
35
+
36
+ ```ruby
37
+ # Routes file
38
+ GET /api/data DataLogic#show auth=session,apikey,oauth
39
+ ```
40
+
41
+ - Strategies execute left-to-right
42
+ - First success wins (remaining strategies skipped)
43
+ - Returns 401 only if all strategies fail
44
+ - Put fastest/most-common strategies first
45
+
46
+ ### Two-Layer Authorization
47
+
48
+ **Layer 1: Route-Level (RouteAuthWrapper)**
49
+ - Use `auth=` for authentication strategies
50
+ - Use `role=` for role-based access (OR logic: `role=admin,editor`)
51
+ - Fast execution (no database queries)
52
+ - Returns 401 (authentication) or 403 (authorization)
53
+
54
+ **Layer 2: Resource-Level (Logic classes)**
55
+ - Handled in `raise_concerns` method
56
+ - Checks ownership, relationships, resource attributes
57
+ - Raises `Otto::Security::AuthorizationError` for 403 response
58
+
59
+ ```ruby
60
+ # Route-level
61
+ GET /admin/users AdminLogic auth=session role=admin
62
+
63
+ # Resource-level in Logic class
64
+ def raise_concerns
65
+ @post = Post.find(params[:id])
66
+ unless @post.user_id == @context.user_id
67
+ raise Otto::Security::AuthorizationError, "Cannot edit another user's post"
68
+ end
69
+ end
70
+ ```
71
+
72
+ ## Configuration Freezing
73
+
74
+ Otto automatically freezes all configuration after first request to prevent runtime security bypasses. Multi-step initialization must complete before first request.
75
+
76
+ ## IP Privacy (Privacy by Default)
77
+
78
+ Otto automatically masks public IP addresses while preserving private/localhost IPs for development:
79
+
80
+ - `IPPrivacyMiddleware` runs FIRST in middleware stack
81
+ - Replaces `env` values directly (REMOTE_ADDR, HTTP_USER_AGENT, HTTP_REFERER)
82
+ - Public IPs masked (192.0.2.100 → 192.0.2.0)
83
+ - Private IPs never masked (127.0.0.1, 192.168.x.x, 10.x.x.x)
84
+ - Supports proxy resolution with trusted proxy configuration
85
+
86
+ For multi-app architectures, add to common middleware stack before logging/monitoring.
87
+
88
+ ## Structured Logging
89
+
90
+ Use explicit structured logging with timing:
91
+
92
+ ```ruby
93
+ Otto.structured_log(:debug, "Route matched",
94
+ Otto::LoggingHelpers.request_context(env).merge(
95
+ type: 'literal',
96
+ handler: route.definition
97
+ )
98
+ )
99
+
100
+ # For timed operations
101
+ Otto::LoggingHelpers.log_timed_operation(:info, "Operation", env, key: value) do
102
+ perform_operation()
103
+ end
104
+ ```
105
+
106
+ - All timing in microseconds via `Otto::Utils.now_in_μs`
107
+ - Use `request_context(env).merge()` pattern for consistency
108
+ - Avoid abstraction layers or event classes
4
109
 
5
110
  ## Development Commands
6
111
 
7
- ### Setup
8
112
  ```bash
9
- # Install development and test dependencies
10
- bundle config set with 'development test'
11
113
  bundle install
12
-
13
- # Lint code
14
114
  bundle exec rubocop
15
-
16
- # Run tests
17
115
  bundle exec rspec
18
-
19
- # Run a specific test
20
- bundle exec rspec spec/path/to/specific_spec.rb
21
- # rspec settings in .rspec
22
116
  ```
23
117
 
24
- ## Project Overview
25
-
26
- ### Core Components
27
- - Ruby Rack-based web framework for defining web applications
28
- - Focuses on security and simplicity
29
- - Supports internationalization and optional security features
30
-
31
- ### Key Features
32
- - Plain-text routes configuration
33
- - Automatic locale detection
34
- - Optional security features:
35
- - CSRF protection
36
- - Input validation
37
- - Security headers
38
- - Trusted proxy configuration
39
-
40
- ### Test Frameworks
41
- - RSpec for unit and integration testing
42
- - Tryouts for behavior-driven testing
43
-
44
- ### Development Tools
45
- - Rubocop for linting
46
- - Debug gem for debugging
47
- - Tryouts for alternative testing approach
48
-
49
- ### Ruby Version Requirements
50
- - Ruby 3.2+
51
- - Rack 3.1+
52
-
53
- ### Important Notes
54
- - Always validate and sanitize user inputs
55
- - Leverage built-in security features
56
- - Use locale helpers for internationalization support
118
+ ## Key Architecture Principles
119
+
120
+ - **Security by Default**: IP privacy, configuration freezing, backtrace sanitization
121
+ - **Privacy by Default**: Public IP masking, no original value storage
122
+ - **Explicit over Implicit**: Direct logging calls, clear configuration
123
+ - **Handler-Level Auth**: Not middleware-based authentication
124
+ - **Two-Layer Authorization**: Route-level + resource-level separation
125
+ - **Rack Integration**: Standard Rack patterns and compatibility
126
+
127
+ See `docs/` directory for comprehensive documentation.
data/Gemfile CHANGED
@@ -14,6 +14,7 @@ gem 'rackup'
14
14
  group :test do
15
15
  gem 'rack-test'
16
16
  gem 'rspec', '~> 3.13'
17
+ gem 'user_agent_parser', '~> 2.18' # Validate anonymized UAs preserve semantic info
17
18
  end
18
19
 
19
20
  # bundle config set with 'optional'
@@ -21,16 +22,18 @@ group :development, :test, optional: true do
21
22
  # Keep gems that need to be in both environments
22
23
  gem 'json_schemer'
23
24
  gem 'rack-attack'
25
+ gem 'reek', '~> 6.5'
24
26
  end
25
27
 
26
28
  group :development do
29
+ gem 'benchmark'
27
30
  gem 'debug'
28
- gem 'rubocop', '~> 1.81.1', require: false
31
+ gem 'rubocop', '~> 1.81.7', require: false
29
32
  gem 'rubocop-performance', require: false
30
33
  gem 'rubocop-rspec', require: false
31
34
  gem 'rubocop-thread_safety', require: false
32
35
  gem 'ruby-lsp', require: false
33
36
  gem 'stackprof', require: false
34
37
  gem 'syntax_tree', require: false
35
- gem 'tryouts', '~> 3.6.0', require: false
38
+ gem 'tryouts', '~> 3.7.1', require: false
36
39
  end