otto 2.3.0 → 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 (44) 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 +33 -10
  5. data/.github/workflows/claude.yml +9 -2
  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 +84 -0
  12. data/Gemfile +4 -1
  13. data/Gemfile.lock +23 -15
  14. data/README.md +96 -0
  15. data/Rakefile +21 -0
  16. data/docs/.gitignore +1 -0
  17. data/docs/migrating/v2.3.0.md +50 -3
  18. data/docs/reverse-proxy-network-services.md +358 -0
  19. data/examples/caddy_tls_demo/README.md +100 -0
  20. data/examples/caddy_tls_demo/app.rb +41 -0
  21. data/examples/caddy_tls_demo/config.ru +31 -0
  22. data/examples/caddy_tls_demo/routes +9 -0
  23. data/examples/caddy_tls_demo/standalone.ru +38 -0
  24. data/lib/otto/caddy_tls/core.rb +74 -0
  25. data/lib/otto/caddy_tls/localhost_guard.rb +158 -0
  26. data/lib/otto/caddy_tls/server.rb +149 -0
  27. data/lib/otto/caddy_tls.rb +7 -0
  28. data/lib/otto/core/configuration.rb +9 -1
  29. data/lib/otto/core/middleware_management.rb +7 -7
  30. data/lib/otto/core/middleware_stack.rb +39 -5
  31. data/lib/otto/core/router.rb +4 -8
  32. data/lib/otto/security/config.rb +293 -2
  33. data/lib/otto/security/configurator.rb +53 -0
  34. data/lib/otto/security/core.rb +62 -0
  35. data/lib/otto/security/csp/parser.rb +120 -0
  36. data/lib/otto/security/csp/report.rb +147 -0
  37. data/lib/otto/security/csp/report_middleware.rb +120 -0
  38. data/lib/otto/security/csp.rb +19 -0
  39. data/lib/otto/security/middleware/ip_privacy_middleware.rb +72 -7
  40. data/lib/otto/security.rb +1 -0
  41. data/lib/otto/utils.rb +133 -18
  42. data/lib/otto/version.rb +1 -1
  43. data/lib/otto.rb +26 -3
  44. metadata +24 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50aa75316f4a3f73a0784cf78fb29e05c030901fa75ac1b8285a6ec0ad89fc3b
4
- data.tar.gz: 38c18d2dc357589e1377a5df0cecd0de26370b002061d9bde6d66994b4990610
3
+ metadata.gz: e50cca678689db3e2447e76a8a1b12f3b709305d4f73908cb06dfe6883adc596
4
+ data.tar.gz: a6093388b74ffbd098c222a026c1f1cf439c8039c75a0d55b663a0874669c6ee
5
5
  SHA512:
6
- metadata.gz: e5f9c0693f83c423a77b20ac88c6af74db6c3d4a9ea345304085b31249a1d20537d64d5c091c5ffb5f6382bfd3fe291d4bcc85f5949928ef09ab8ae74fe09c3b
7
- data.tar.gz: 29fb704139356b0e0df805567e761593b10faf519f7977087425f9752711dfe4315b41266d864c071779f1e37b55e1a9d40c454c74f5973f728f3f1e1ff1bb97
6
+ metadata.gz: ea5a8acda9a29c09d3a16a050050c2e9b9a1517c7f63e24bb7bc910801a7a07874fff1215550a740cef2c9c3698bc77378083b33f47794a7668b77feff9e6e67
7
+ data.tar.gz: 62cfbe824d5d0d9e159424a6d930217f05b8b01de7142108a6a80953207cca9037a3e63d6ae5c16585dda3ff7b8a9b2d070b593f748563c0a5a20ed852add37f
@@ -10,6 +10,6 @@ updates:
10
10
  schedule:
11
11
  interval: "weekly"
12
12
  - package-ecosystem: "bundler"
13
- directory: "/"
13
+ directory: "/" # Location of package manifests
14
14
  schedule:
15
15
  interval: "weekly"
@@ -37,6 +37,9 @@ jobs:
37
37
  # `require_relative 'file/write.rb'` against a file deleted in
38
38
  # the same release, the reason 2.0.2 exists.
39
39
  include:
40
+ - ruby: "3.2"
41
+ experimental: false
42
+ lockfile: "locked"
40
43
  - ruby: "3.3"
41
44
  experimental: false
42
45
  lockfile: "locked"
@@ -49,6 +52,9 @@ jobs:
49
52
  - ruby: "4.0"
50
53
  experimental: true
51
54
  lockfile: "locked"
55
+ - ruby: "3.2"
56
+ experimental: false
57
+ lockfile: "unlocked"
52
58
  - ruby: "3.3"
53
59
  experimental: false
54
60
  lockfile: "unlocked"
@@ -63,7 +69,7 @@ jobs:
63
69
  lockfile: "unlocked"
64
70
 
65
71
  steps:
66
- - uses: actions/checkout@v6.0.3
72
+ - uses: actions/checkout@v7.0.0
67
73
  - name: Set up Ruby
68
74
  uses: ruby/setup-ruby@v1
69
75
  continue-on-error: ${{ matrix.experimental }}
@@ -4,6 +4,16 @@ on:
4
4
  pull_request:
5
5
  types: [opened, synchronize, labeled]
6
6
  workflow_dispatch:
7
+ inputs:
8
+ model:
9
+ description: "Claude model for this run (overrides the CLAUDE_MODEL repo variable)"
10
+ type: choice
11
+ required: false
12
+ default: claude-opus-4-6
13
+ options:
14
+ - claude-opus-4-6
15
+ - claude-sonnet-4-6
16
+ - claude-haiku-4-5-20251001
7
17
 
8
18
  jobs:
9
19
  claude-review:
@@ -14,10 +24,21 @@ jobs:
14
24
  # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
15
25
 
16
26
  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'))
27
+ # Run only when ALL of the following hold:
28
+ # 1. Author is not a bot (renovate, dependabot, etc. surface as 'name[bot]').
29
+ # 2. PR head is not from a fork. Fork PRs run without repository secrets, so
30
+ # CLAUDE_CODE_OAUTH_TOKEN is empty and the action would fail.
31
+ # 3. The event is a freshly opened PR, a 'claude-review' label add, or a push
32
+ # to a PR that already carries the 'claude-review' label.
33
+ if: ${{
34
+ !endsWith(github.actor, '[bot]') &&
35
+ !github.event.pull_request.head.repo.fork &&
36
+ (
37
+ (github.event.action == 'opened') ||
38
+ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
39
+ (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'claude-review'))
40
+ )
41
+ }}
21
42
 
22
43
  permissions:
23
44
  contents: read
@@ -27,7 +48,7 @@ jobs:
27
48
 
28
49
  steps:
29
50
  - name: Checkout repository
30
- uses: actions/checkout@v6.0.3
51
+ uses: actions/checkout@v7.0.0
31
52
  with:
32
53
  fetch-depth: 1
33
54
 
@@ -37,10 +58,12 @@ jobs:
37
58
  with:
38
59
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
60
 
40
- # Fall back to Sonnet when the primary model is unavailable/overloaded.
41
- # Without this, an unavailable primary surfaces as "Claude encountered
42
- # an error" and fails the claude-review check.
43
- fallback_model: "claude-sonnet-4-6"
61
+ # Primary model precedence: the workflow_dispatch "model" input (manual
62
+ # runs) -> the CLAUDE_MODEL repo variable -> this built-in default.
63
+ # Pinning a current id avoids the action's frozen default, which 404s
64
+ # ("model: claude-sonnet-4-20250514"). Fall back to Sonnet on overload.
65
+ model: "${{ inputs.model || vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
66
+ fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
44
67
 
45
68
  # Direct prompt for automated review (no @claude mention needed)
46
69
  direct_prompt: |
@@ -51,7 +74,7 @@ jobs:
51
74
  - Security concerns
52
75
  - Test coverage
53
76
 
54
- Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
77
+ Use the repository's CLAUDE.md or AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
55
78
 
56
79
  # Use sticky comments to reuse the same comment on subsequent pushes to the same PR
57
80
  use_sticky_comment: true
@@ -26,16 +26,23 @@ jobs:
26
26
  actions: read # Required for Claude to read CI results on PRs
27
27
  steps:
28
28
  - name: Checkout repository
29
- uses: actions/checkout@v6.0.3
29
+ uses: actions/checkout@v7.0.0
30
30
  with:
31
31
  fetch-depth: 1
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@v1
35
+ uses: anthropics/claude-code-action@beta
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
39
+ # Primary model: set the CLAUDE_MODEL repo variable to override without
40
+ # editing this file. Pinning a current id avoids the action's frozen
41
+ # default, which 404s ("model: claude-sonnet-4-20250514"). Fall back to
42
+ # Sonnet if the primary is unavailable or overloaded.
43
+ model: "${{ vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
44
+ fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
45
+
39
46
  # This is an optional setting that allows Claude to read CI results on PRs
40
47
  additional_permissions: |
41
48
  actions: read
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Checkout code
24
- uses: actions/checkout@v6.0.3
24
+ uses: actions/checkout@v7.0.0
25
25
 
26
26
  - name: Set up Ruby
27
27
  uses: ruby/setup-ruby@v1
@@ -88,7 +88,7 @@ jobs:
88
88
 
89
89
  steps:
90
90
  - name: Checkout code
91
- uses: actions/checkout@v6.0.3
91
+ uses: actions/checkout@v7.0.0
92
92
 
93
93
  - name: Set up Ruby
94
94
  uses: ruby/setup-ruby@v1
@@ -114,6 +114,16 @@ jobs:
114
114
  runs-on: ubuntu-latest
115
115
  timeout-minutes: 10
116
116
 
117
+ # `rake` lives in the optional :development group (see Gemfile), so a plain
118
+ # `bundle install` skips it and rubygems/release-gem's `bundle exec rake
119
+ # release` dies with "rake is not currently included in the bundle". Opt the
120
+ # group in for the whole job: setup-ruby's cached `bundle install` reads
121
+ # BUNDLE_WITH so rake is installed, and the release step's `bundle exec`
122
+ # reads it too so rake stays in the resolved bundle. This mirrors the
123
+ # `bundle config set --local with` that ci.yml uses for the same groups.
124
+ env:
125
+ BUNDLE_WITH: development
126
+
117
127
  environment:
118
128
  name: rubygems.org
119
129
  url: https://rubygems.org/gems/otto
@@ -124,7 +134,7 @@ jobs:
124
134
 
125
135
  steps:
126
136
  - name: Checkout
127
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
137
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
128
138
  with:
129
139
  persist-credentials: false
130
140
 
@@ -159,4 +169,4 @@ jobs:
159
169
  echo "Releasing otto ${gem_version} from tag ${RELEASE_TAG}"
160
170
 
161
171
  - name: Build and push gem to RubyGems
162
- uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
172
+ uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
@@ -0,0 +1,66 @@
1
+ # .github/workflows/ruby-lint.yml
2
+
3
+ name: Ruby Lint
4
+
5
+ # Runs RuboCop across the supported Ruby versions. Linting is informational:
6
+ # every dependency/lint step is continue-on-error, so style findings surface as
7
+ # annotations without blocking merges.
8
+
9
+ on:
10
+ push:
11
+ branches:
12
+ - fix/*
13
+ - rel/*
14
+ pull_request:
15
+ branches:
16
+ - main
17
+ - develop
18
+ - feature/*
19
+ workflow_dispatch:
20
+ inputs:
21
+ debug_enabled:
22
+ type: boolean
23
+ description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
24
+ required: false
25
+ default: false
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ lint:
32
+ timeout-minutes: 10 # prevent hung jobs
33
+
34
+ runs-on: ubuntu-24.04
35
+
36
+ strategy:
37
+ fail-fast: true
38
+ matrix:
39
+ ruby: ['3.2', '3.3', '3.4', '3.5', '4.0']
40
+ continue-on-error: [true]
41
+
42
+ steps:
43
+ - name: Checkout code
44
+ uses: actions/checkout@v6.0.3
45
+
46
+ - uses: ruby/setup-ruby@v1
47
+ with:
48
+ ruby-version: ${{ matrix.ruby }}
49
+ bundler-cache: true
50
+
51
+ - name: Setup tmate session
52
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3
53
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
54
+ with:
55
+ detached: true
56
+
57
+ - name: Install dependencies
58
+ continue-on-error: ${{ matrix.continue-on-error }}
59
+ run: |
60
+ bundle config path vendor/bundle
61
+ bundle install --jobs 4 --retry 3
62
+
63
+ - name: Run Rubocop
64
+ continue-on-error: ${{ matrix.continue-on-error }}
65
+ run: |
66
+ bundle exec rubocop --config .rubocop.yml --format json --fail-level warning
@@ -0,0 +1,117 @@
1
+ name: Generate and Deploy YARD Documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'lib/**/*'
9
+ - 'docs/**/*'
10
+ - 'README.md'
11
+ - 'CHANGELOG.md'
12
+ - 'CHANGELOG.rst'
13
+ - 'LICENSE.txt'
14
+ - '.yardopts'
15
+ - '.github/workflows/yardoc.yml'
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+ pages: write
21
+ id-token: write
22
+
23
+ # Allow only one concurrent deployment, skipping runs queued between the run
24
+ # in-progress and latest queued. Do NOT cancel in-progress runs so production
25
+ # deployments can complete.
26
+ concurrency:
27
+ group: "pages"
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ build-docs:
32
+ timeout-minutes: 10
33
+ runs-on: ubuntu-latest
34
+ name: Generate YARD Documentation
35
+ env:
36
+ # otto declares yard/kramdown in a `group :development` whose name also
37
+ # appears in an `optional: true` block, so Bundler treats `development`
38
+ # as optional and the default `bundle install` skips it (yard never
39
+ # installs -> `bundle exec yard` is "command not found"). Mirror otto's
40
+ # ci.yml convention so setup-ruby installs the dev/test gems.
41
+ BUNDLE_WITH: "development:test"
42
+
43
+ steps:
44
+ - name: Checkout repository
45
+ uses: actions/checkout@v6.0.3
46
+ with:
47
+ fetch-depth: 0
48
+
49
+ - name: Set up Ruby environment
50
+ uses: ruby/setup-ruby@v1
51
+ with:
52
+ ruby-version: '3.4'
53
+ bundler-cache: true
54
+
55
+ - name: Generate documentation
56
+ # Uses the repository's committed .yardopts as the single source of
57
+ # truth for the output dir, includes/excludes, markup, and tags.
58
+ run: |
59
+ echo "::group::YARD Documentation Generation"
60
+ bundle exec yard stats --list-undoc || true
61
+ bundle exec yard doc
62
+ echo "::endgroup::"
63
+
64
+ - name: Disable Jekyll processing
65
+ # YARD emits files and directories that begin with underscores; the
66
+ # .nojekyll marker stops GitHub Pages from stripping them.
67
+ run: touch doc/.nojekyll
68
+
69
+ - name: Validate documentation output
70
+ run: |
71
+ echo "::group::Documentation Validation"
72
+ if [ ! -d "doc" ]; then
73
+ echo "Error: documentation directory 'doc' was not generated." >&2
74
+ echo "Ensure .yardopts sets '--output-dir doc'." >&2
75
+ exit 1
76
+ fi
77
+ if [ ! -f "doc/index.html" ]; then
78
+ echo "Warning: doc/index.html not found"
79
+ fi
80
+ echo "Generated HTML files: $(find doc -name '*.html' | wc -l)"
81
+ echo "Total documentation size: $(du -sh doc/ | cut -f1)"
82
+ echo "::endgroup::"
83
+
84
+ - name: Setup GitHub Pages configuration
85
+ uses: actions/configure-pages@v6
86
+
87
+ - name: Upload documentation artifact
88
+ uses: actions/upload-pages-artifact@v5
89
+ with:
90
+ path: './doc'
91
+
92
+ deploy-pages:
93
+ timeout-minutes: 10
94
+ environment:
95
+ name: github-pages
96
+ url: ${{ steps.deployment.outputs.page_url }}
97
+ runs-on: ubuntu-latest
98
+ needs: build-docs
99
+ outputs:
100
+ page_url: ${{ steps.deployment.outputs.page_url }}
101
+
102
+ steps:
103
+ - name: Deploy to GitHub Pages
104
+ id: deployment
105
+ uses: actions/deploy-pages@v5
106
+
107
+ notify-completion:
108
+ timeout-minutes: 5
109
+ runs-on: ubuntu-latest
110
+ needs: [build-docs, deploy-pages]
111
+ if: success()
112
+
113
+ steps:
114
+ - name: Documentation deployment summary
115
+ run: |
116
+ echo "::notice title=Documentation Deployed::YARD documentation successfully deployed to GitHub Pages"
117
+ echo "::notice title=Access URL::Documentation available at: ${{ needs.deploy-pages.outputs.page_url }}"
data/.yardopts ADDED
@@ -0,0 +1,15 @@
1
+ --readme README.md
2
+ --title "Otto Ruby Library Documentation"
3
+ --protected
4
+ --no-private
5
+ --markup markdown
6
+ --markup-provider kramdown
7
+ --output-dir doc
8
+ --exclude lib/otto/version.rb
9
+ lib/**/*.rb
10
+ -
11
+ README.md
12
+ CHANGELOG.rst
13
+ LICENSE.txt
14
+ docs/**/*.md
15
+ examples/*.rb
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,90 @@ 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.4.0:
11
+
12
+ 2.4.0 — 2026-07-01
13
+ ==================
14
+
15
+ Added
16
+ -----
17
+
18
+ - ``Otto#enable_csp_reporting!(report_uri, endpoint_url: nil, &block)`` —
19
+ turnkey CSP violation reporting. Emits a ``report-uri`` directive and, with
20
+ ``endpoint_url:``, a ``report-to`` directive plus ``Reporting-Endpoints``
21
+ header. Parses legacy ``application/csp-report`` and Reporting API
22
+ ``application/reports+json`` payloads into ``Otto::Security::CSP::Report``
23
+ and invokes the callback per violation. Opt-in. (delano/otto#174)
24
+
25
+ - ``MiddlewareStack`` ``:outermost`` position, for middleware that must run
26
+ ahead of all others regardless of registration order.
27
+
28
+ - ``Otto::CaddyTLS``: an opt-in Caddy on-demand TLS permission endpoint,
29
+ enabled with ``otto.enable_caddy_tls! { |domain| ... }``. (delano/otto#175)
30
+
31
+ Fixed
32
+ -----
33
+
34
+ - ``IPPrivacyMiddleware`` no longer writes ``nil`` into CGI-style Rack env
35
+ keys (e.g. ``HTTP_REFERER``, ``HTTP_USER_AGENT``, ``REMOTE_ADDR``) when
36
+ redacting request data, which violated the Rack SPEC and tripped
37
+ ``Rack::Lint``. Empty anonymized values now delete the key instead of
38
+ setting it to ``nil``, and a request with no resolvable client IP no
39
+ longer gets a ``nil`` ``REMOTE_ADDR``. (delano/otto#167)
40
+
41
+ - ``Otto::Security::CSP::ReportMiddleware`` no longer turns a downstream
42
+ error on a non-report request into an empty ``204``.
43
+
44
+ Security
45
+ --------
46
+
47
+ - The ``Otto::CaddyTLS`` permission endpoint is loopback-only by default and
48
+ fails closed. (delano/otto#175)
49
+
50
+ - Security middleware registered through the ``otto.security.*``
51
+ Configurator after ``Otto.new`` now actually runs on the request chain —
52
+ previously CSRF, request validation, rate limiting, and CSP reporting
53
+ silently went unenforced.
54
+
55
+ AI Assistance
56
+ -------------
57
+
58
+ - CSP violation reporting (``report-uri`` / ``report-to``), the
59
+ ``:outermost`` middleware position, and the Configurator
60
+ middleware-registration fix were designed and implemented with AI
61
+ assistance.
62
+
63
+ - ``Otto::CaddyTLS`` designed, implemented, and reviewed with AI assistance.
64
+
65
+ - The Rack SPEC ``nil``-into-CGI-key fix — including the sibling
66
+ ``REMOTE_ADDR`` masking bug and ``Rack::Lint`` test coverage — diagnosed
67
+ and fixed with AI assistance.
68
+
69
+ .. _changelog-2.3.1:
70
+
71
+ 2.3.1 — 2026-06-22
72
+ ==================
73
+
74
+ Added
75
+ -----
76
+
77
+ - Depth mode (``Otto::Security::Config#trusted_proxy_depth``) can now count hops
78
+ from a configurable forwarded header, via a new ``#trusted_proxy_header``
79
+ accessor: ``X-Forwarded-For`` (default), ``Forwarded`` (RFC 7239), or ``Both``
80
+ (``Forwarded`` when it carries a ``for=``, otherwise ``X-Forwarded-For``).
81
+ Settable through ``Otto::Security::Configurator#configure`` and the
82
+ ``trusted_proxy_header`` option of ``Otto.new`` / ``configure_security``; an
83
+ unrecognized value raises ``ArgumentError`` at assignment. This reaches parity
84
+ with OneTimeSecret's ``site.network.trusted_proxy.header``. (delano/otto#150,
85
+ onetimesecret#3436)
86
+
87
+ AI Assistance
88
+ -------------
89
+
90
+ - RFC 7239 ``Forwarded`` / ``Both`` depth-header support designed and implemented
91
+ with AI pair programming, with per-header, quoted-IPv6, and ``Both``-precedence
92
+ test coverage.
93
+
10
94
  .. _changelog-2.3.0:
11
95
 
12
96
  2.3.0 — 2026-06-21
data/Gemfile CHANGED
@@ -27,7 +27,8 @@ group :development do
27
27
  gem 'benchmark'
28
28
  gem 'debug'
29
29
  gem 'rackup' # Used to boot examples/ apps; not needed by specs
30
- gem 'rubocop', '~> 1.86.2', require: false
30
+ gem 'rake', '~> 13.4', require: false # Provides `rake release` for release-gem.yml
31
+ gem 'rubocop', '~> 1.88.0', require: false
31
32
  gem 'rubocop-performance', require: false
32
33
  gem 'rubocop-rspec', require: false
33
34
  gem 'rubocop-thread_safety', require: false
@@ -35,4 +36,6 @@ group :development do
35
36
  gem 'stackprof', require: false
36
37
  gem 'syntax_tree', require: false
37
38
  gem 'tryouts', '~> 3.7.1', require: false
39
+ gem 'yard', '~> 0.9', require: false # API docs for yardoc.yml
40
+ gem 'kramdown', require: false # Markdown provider for YARD
38
41
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (2.3.0)
4
+ otto (2.4.0)
5
5
  concurrent-ruby (~> 1.3, < 2.0)
6
6
  logger (~> 1, < 2.0)
7
7
  loofah (~> 2.20)
@@ -15,7 +15,7 @@ GEM
15
15
  ast (2.4.3)
16
16
  benchmark (0.5.0)
17
17
  bigdecimal (4.1.1)
18
- concurrent-ruby (1.3.6)
18
+ concurrent-ruby (1.3.7)
19
19
  crass (1.0.6)
20
20
  date (3.5.1)
21
21
  debug (1.11.1)
@@ -59,12 +59,14 @@ GEM
59
59
  prism (>= 1.3.0)
60
60
  rdoc (>= 4.0.0)
61
61
  reline (>= 0.4.2)
62
- json (2.19.5)
62
+ json (2.19.9)
63
63
  json_schemer (2.5.0)
64
64
  bigdecimal
65
65
  hana (~> 1.3)
66
66
  regexp_parser (~> 2.0)
67
67
  simpleidn (~> 0.2)
68
+ kramdown (2.5.2)
69
+ rexml (>= 3.4.4)
68
70
  language_server-protocol (3.17.0.5)
69
71
  lint_roller (1.1.0)
70
72
  logger (1.7.0)
@@ -72,21 +74,21 @@ GEM
72
74
  crass (~> 1.0.2)
73
75
  nokogiri (>= 1.12.0)
74
76
  minitest (5.26.0)
75
- nokogiri (1.19.2-aarch64-linux-gnu)
77
+ nokogiri (1.19.4-aarch64-linux-gnu)
76
78
  racc (~> 1.4)
77
- nokogiri (1.19.2-aarch64-linux-musl)
79
+ nokogiri (1.19.4-aarch64-linux-musl)
78
80
  racc (~> 1.4)
79
- nokogiri (1.19.2-arm-linux-gnu)
81
+ nokogiri (1.19.4-arm-linux-gnu)
80
82
  racc (~> 1.4)
81
- nokogiri (1.19.2-arm-linux-musl)
83
+ nokogiri (1.19.4-arm-linux-musl)
82
84
  racc (~> 1.4)
83
- nokogiri (1.19.2-arm64-darwin)
85
+ nokogiri (1.19.4-arm64-darwin)
84
86
  racc (~> 1.4)
85
- nokogiri (1.19.2-x86_64-darwin)
87
+ nokogiri (1.19.4-x86_64-darwin)
86
88
  racc (~> 1.4)
87
- nokogiri (1.19.2-x86_64-linux-gnu)
89
+ nokogiri (1.19.4-x86_64-linux-gnu)
88
90
  racc (~> 1.4)
89
- nokogiri (1.19.2-x86_64-linux-musl)
91
+ nokogiri (1.19.4-x86_64-linux-musl)
90
92
  racc (~> 1.4)
91
93
  parallel (1.28.0)
92
94
  parser (3.3.11.1)
@@ -113,6 +115,7 @@ GEM
113
115
  rackup (2.3.1)
114
116
  rack (>= 3)
115
117
  rainbow (3.1.1)
118
+ rake (13.4.2)
116
119
  rbs (4.0.2)
117
120
  logger
118
121
  prism (>= 1.6.0)
@@ -144,7 +147,7 @@ GEM
144
147
  diff-lcs (>= 1.2.0, < 2.0)
145
148
  rspec-support (~> 3.13.0)
146
149
  rspec-support (3.13.7)
147
- rubocop (1.86.2)
150
+ rubocop (1.88.0)
148
151
  json (~> 2.3)
149
152
  language_server-protocol (~> 3.17.0.2)
150
153
  lint_roller (~> 1.1.0)
@@ -162,9 +165,10 @@ GEM
162
165
  lint_roller (~> 1.1)
163
166
  rubocop (>= 1.75.0, < 2.0)
164
167
  rubocop-ast (>= 1.47.1, < 2.0)
165
- rubocop-rspec (3.9.0)
168
+ rubocop-rspec (3.10.2)
166
169
  lint_roller (~> 1.1)
167
- rubocop (~> 1.81)
170
+ regexp_parser (>= 2.0)
171
+ rubocop (~> 1.86, >= 1.86.2)
168
172
  rubocop-thread_safety (0.7.3)
169
173
  lint_roller (~> 1.1)
170
174
  rubocop (~> 1.72, >= 1.72.1)
@@ -196,6 +200,7 @@ GEM
196
200
  unicode-emoji (~> 4.1)
197
201
  unicode-emoji (4.2.0)
198
202
  user_agent_parser (2.21.0)
203
+ yard (0.9.44)
199
204
  zeitwerk (2.7.3)
200
205
 
201
206
  PLATFORMS
@@ -212,13 +217,15 @@ DEPENDENCIES
212
217
  benchmark
213
218
  debug
214
219
  json_schemer
220
+ kramdown
215
221
  otto!
216
222
  rack-attack
217
223
  rack-test
218
224
  rackup
225
+ rake (~> 13.4)
219
226
  reek (~> 6.5)
220
227
  rspec (~> 3.13)
221
- rubocop (~> 1.86.2)
228
+ rubocop (~> 1.88.0)
222
229
  rubocop-performance
223
230
  rubocop-rspec
224
231
  rubocop-thread_safety
@@ -227,6 +234,7 @@ DEPENDENCIES
227
234
  syntax_tree
228
235
  tryouts (~> 3.7.1)
229
236
  user_agent_parser (~> 2.18)
237
+ yard (~> 0.9)
230
238
 
231
239
  BUNDLED WITH
232
240
  2.7.1