style_capsule 2.0.1 → 2.0.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +27 -12
- data/SECURITY.md +11 -14
- data/docs/changelogs/20260904180000_audit-fixes.md +29 -0
- data/docs/changelogs/20260925094000_head-injection-bufferable-chunked.md +25 -0
- data/docs/dependencies/20260904174500_json-cve-2026-71847.md +37 -0
- data/docs/issues/20260904174500_engineering-and-dependency-audit.md +47 -0
- data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +28 -0
- data/lib/style_capsule/css_file_writer.rb +5 -4
- data/lib/style_capsule/css_processor.rb +16 -2
- data/lib/style_capsule/head_injection_middleware.rb +18 -6
- data/lib/style_capsule/stylesheet_registry.rb +85 -60
- data/lib/style_capsule/version.rb +1 -1
- data/sig/style_capsule.rbs +2 -0
- metadata +16 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afc951f02fe29cd319b521b363049d3dba5e607099216e1782f48b4578f06f4b
|
|
4
|
+
data.tar.gz: 85916b390e95a3109da5a481f6b193a44309864499b5d3cb4ec4a634960c0ccd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf300d946cf83d68e40a16862698bd8354f8c51dd927e2fb4e6cb8fd9e3f1d6c6298bb545233132399ea496985e1a1b45b9279bae7b9467663a525ef4893f530
|
|
7
|
+
data.tar.gz: 95450d3eeef09959b75dca145f06abfe9f5e59619044d3456e43a1c073adac4e51e1c94e59187ddee72ba823de54c4cc640754d80373fdf1e18316e4b7046bb9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 2.0.2 (2026-09-25)
|
|
4
|
+
|
|
5
|
+
- Fix `HeadInjectionMiddleware` dropping pending stylesheets on bufferable chunked HTML
|
|
6
|
+
- Inject when the Rack body responds to `to_ary`; skip only non-bufferable streaming bodies
|
|
7
|
+
- After rewrite, set `Content-Length` and remove `Transfer-Encoding`
|
|
8
|
+
- Fail closed when CSS would close a style tag before it is written into HTML
|
|
9
|
+
- Escape allowed attributes on the non-Rails stylesheet link fallback
|
|
10
|
+
- Bound the inline stylesheet cache
|
|
11
|
+
- Align documented Ruby and Rails floors with the gemspec
|
|
4
12
|
|
|
5
13
|
## 2.0.1 (2026-07-03)
|
|
6
14
|
|
data/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# style_capsule
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/style_capsule) [](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [](https://app.codecov.io/github/amkisko/style_capsule.rb)
|
|
4
4
|
|
|
5
5
|
CSS scoping extension for Ruby components. Provides attribute-based style encapsulation for Phlex, ViewComponent, and ERB templates to prevent style leakage between components. Works with Rails and can be used standalone in other Ruby frameworks (Sinatra, Hanami, etc.) or plain Ruby scripts. Includes configurable caching strategies for optimal performance.
|
|
6
6
|
|
|
7
|
+
Design changes go through `rfcs/` (RFC 0001).
|
|
8
|
+
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
9
11
|
Add to your Gemfile:
|
|
@@ -256,7 +258,7 @@ By default, `StyleCapsule::HeadInjectionMiddleware` appends pending request-scop
|
|
|
256
258
|
config.style_capsule.head_injection_middleware = false
|
|
257
259
|
```
|
|
258
260
|
|
|
259
|
-
The middleware
|
|
261
|
+
The middleware only rewrites bufferable bodies (Rack bodies that respond to `to_ary`). Chunked HTML with a bufferable body is injected, then returned with `Content-Length` and without `Transfer-Encoding`. Non-bufferable streaming bodies (ActionController::Live, SSE, and similar) are left alone; disable the middleware and inject manually with `StyleCapsule::StylesheetRegistry.inject_pending_head_stylesheets` if needed. The middleware does not buffer when no pending request-scoped stylesheets remain. When it does rewrite, it holds the full 2xx HTML body in memory.
|
|
260
262
|
|
|
261
263
|
## Caching Strategies
|
|
262
264
|
|
|
@@ -317,11 +319,11 @@ StyleCapsule::CssFileWriter.configure(
|
|
|
317
319
|
filename_pattern: ->(component_class, capsule_id) {
|
|
318
320
|
"capsule-#{capsule_id}.css"
|
|
319
321
|
},
|
|
320
|
-
fallback_dir:
|
|
322
|
+
fallback_dir: StyleCapsule::CssFileWriter::FALLBACK_OUTPUT_DIR
|
|
321
323
|
)
|
|
322
324
|
```
|
|
323
325
|
|
|
324
|
-
**Fallback Directory:** In production environments where the app directory is read-only (e.g., Docker containers), StyleCapsule automatically falls back to writing files
|
|
326
|
+
**Fallback Directory:** In production environments where the app directory is read-only (e.g., Docker containers), StyleCapsule automatically falls back to writing files under a per-user directory in `Dir.tmpdir` when the default location is not writable. When using the fallback directory, the gem gracefully falls back to inline CSS rendering, keeping the UI fully functional.
|
|
325
327
|
|
|
326
328
|
**Precompilation:**
|
|
327
329
|
|
|
@@ -452,9 +454,9 @@ end
|
|
|
452
454
|
|
|
453
455
|
## Requirements
|
|
454
456
|
|
|
455
|
-
- Ruby >= 3.
|
|
456
|
-
- Rails >=
|
|
457
|
-
- ActiveSupport >=
|
|
457
|
+
- Ruby >= 3.4
|
|
458
|
+
- Rails >= 7.0, < 9.0 (optional, for Rails integration)
|
|
459
|
+
- ActiveSupport >= 7.0, < 9.0 (optional, for Rails integration)
|
|
458
460
|
|
|
459
461
|
**Note**: The gem can be used without Rails! See [Non-Rails Support](#non-rails-support) below.
|
|
460
462
|
|
|
@@ -478,11 +480,11 @@ bundle install
|
|
|
478
480
|
bundle exec appraisal install
|
|
479
481
|
|
|
480
482
|
# Run tests
|
|
481
|
-
bundle exec rspec
|
|
483
|
+
bundle exec polyrun parallel-rspec --workers 5 --merge-failures
|
|
482
484
|
|
|
483
485
|
# Run tests for all Rails versions
|
|
484
|
-
bundle exec appraisal rails72 rspec
|
|
485
|
-
bundle exec appraisal rails8ruby34 rspec
|
|
486
|
+
bundle exec appraisal rails72 polyrun parallel-rspec --workers 5 --merge-failures
|
|
487
|
+
bundle exec appraisal rails8ruby34 polyrun parallel-rspec --workers 5 --merge-failures
|
|
486
488
|
|
|
487
489
|
# Linting
|
|
488
490
|
bundle exec standardrb --fix
|
|
@@ -513,12 +515,25 @@ gem push style_capsule-*.gem
|
|
|
513
515
|
|
|
514
516
|
StyleCapsule includes security protections:
|
|
515
517
|
- Path traversal protection
|
|
516
|
-
-
|
|
518
|
+
- Capsule id and HTML attribute validation and escaping
|
|
517
519
|
- Size limits (1MB per component)
|
|
518
|
-
-
|
|
520
|
+
- Reject CSS that would close a style tag
|
|
519
521
|
|
|
520
522
|
For detailed security information, see [SECURITY.md](SECURITY.md).
|
|
521
523
|
|
|
524
|
+
## Links
|
|
525
|
+
|
|
526
|
+
- [GitHub](https://github.com/amkisko/style_capsule.rb)
|
|
527
|
+
- [GitLab](https://gitlab.com/amkisko/style_capsule.rb)
|
|
528
|
+
- [RubyGems](https://rubygems.org/gems/style_capsule)
|
|
529
|
+
- [Versions Atom](https://rubygems.org/gems/style_capsule/versions.atom) (feed id `9946895197223`)
|
|
530
|
+
- [libraries.io](https://libraries.io/rubygems/style_capsule)
|
|
531
|
+
- [Deps.dev](https://deps.dev/rubygems/style_capsule)
|
|
532
|
+
- [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_style_capsule.rb)
|
|
533
|
+
- [Snyk](https://snyk.io/test/github/amkisko/style_capsule.rb)
|
|
534
|
+
- [Codecov](https://app.codecov.io/github/amkisko/style_capsule.rb)
|
|
535
|
+
- [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/style_capsule.rb)
|
|
536
|
+
|
|
522
537
|
## License
|
|
523
538
|
|
|
524
539
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/SECURITY.md
CHANGED
|
@@ -2,27 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Reporting a Vulnerability
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Email security details to: security@kiskolabs.com
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Include: description, steps to reproduce, potential impact, and suggested fix (if available).
|
|
7
|
+
Send the report only to that address. Include description, steps to reproduce, potential impact, and a suggested fix when you have one.
|
|
10
8
|
|
|
11
9
|
### Response Timeline
|
|
12
10
|
|
|
13
|
-
- We will acknowledge receipt of your report
|
|
14
|
-
- We will provide an initial assessment
|
|
15
|
-
- We will keep you informed of our progress and resolution timeline
|
|
11
|
+
- We will acknowledge receipt of your report.
|
|
12
|
+
- We will provide an initial assessment.
|
|
13
|
+
- We will keep you informed of our progress and resolution timeline.
|
|
16
14
|
|
|
17
15
|
### Disclosure Policy
|
|
18
16
|
|
|
19
|
-
- We will work with you to understand and resolve the issue
|
|
20
|
-
- We will credit you for the discovery
|
|
21
|
-
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
-
- We will coordinate public disclosure with you
|
|
17
|
+
- We will work with you to understand and resolve the issue.
|
|
18
|
+
- We will credit you for the discovery unless you prefer to remain anonymous.
|
|
19
|
+
- We will publish a security advisory after the vulnerability is patched.
|
|
20
|
+
- We will coordinate public disclosure with you.
|
|
23
21
|
|
|
24
22
|
## Automation Security
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* **Supply Chain:** All automated dependencies must be verified.
|
|
24
|
+
- Keep production credentials, API keys, and personal data out of prompts sent to third-party models or automation services.
|
|
25
|
+
- Verify automated dependencies.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Audit fixes for style_capsule 2.0.1
|
|
2
|
+
|
|
3
|
+
Recorded 2026-09-04. Implements the ranked findings from docs/issues/20260904174500_engineering-and-dependency-audit.md. Shipped in product CHANGELOG 2.0.2. json lockfile bump is recorded in docs/dependencies/20260904174500_json-cve-2026-71847.md.
|
|
4
|
+
|
|
5
|
+
## Decisions
|
|
6
|
+
|
|
7
|
+
Treat stored CSS as untrusted at the style-tag boundary. CssProcessor.reject_style_element_breakout! matches a case-insensitive style closer and raises ArgumentError. Call it after the size check in scope_selectors and scope_with_nesting, and again in render_inline_stylesheet. Empty CSS still returns early without that check.
|
|
8
|
+
|
|
9
|
+
Allowlist non-Rails fallback link attributes. Emit only media, title, crossorigin, integrity, nonce, type, id, class, and data-* names. Escape values with CGI.escapeHTML. Never emit option href, rel, or event handlers. Keep href as /assets/#{validated_path}.css.
|
|
10
|
+
|
|
11
|
+
Cap the inline CSS cache at 256 entries with Hash insertion-order shift. Mutex around cache mutations uses unlocked cleanup internals so cached_inline does not deadlock if a cache proc re-enters. Mutex also wraps register_eager writes and clear_manifest. The process-wide manifest is not capped.
|
|
12
|
+
|
|
13
|
+
CssFileWriter fallback directory is File.join(Dir.tmpdir, "style_capsule-#{Process.uid}"). Specs compare the FALLBACK_OUTPUT_DIR constant.
|
|
14
|
+
|
|
15
|
+
Add pull_request to .github/workflows/test.yml. Drop npm and docker from Dependabot. Do not add fake gemfiles Dependabot directories. Remove unused memory_profiler from the gemspec. Do not split stylesheet_registry.rb in this pass.
|
|
16
|
+
|
|
17
|
+
## Effects
|
|
18
|
+
|
|
19
|
+
Specs added for style-tag breakout in CssProcessor and render_inline_stylesheet, quote and href and onload options on the non-Rails link fallback, and inline cache eviction of the oldest key. bundle exec rspec: 498 examples, 0 failures. bundle-audit check --no-update is clean on all four lockfiles with json 2.21.2.
|
|
20
|
+
|
|
21
|
+
README Requirements match the gemspec (Ruby >= 3.4, Rails >= 7.0, < 9.0). Badge uses v=2.0.1. Security section names style-tag closer rejection and capsule id and HTML attribute validation. Middleware docs state that rewrite buffers the full 2xx HTML body.
|
|
22
|
+
|
|
23
|
+
## Next
|
|
24
|
+
|
|
25
|
+
stylesheet_registry.rb remains over the house line-count ceiling. Split only if a later change owns that file.
|
|
26
|
+
|
|
27
|
+
## Source
|
|
28
|
+
|
|
29
|
+
Parent issue docs/issues/20260904174500_engineering-and-dependency-audit.md. json note docs/dependencies/20260904174500_json-cve-2026-71847.md. Branch patch/audit-fixes. Release 2.0.2.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# HeadInjectionMiddleware injects bufferable chunked HTML
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
HeadInjectionMiddleware no longer skips every response with Transfer-Encoding chunked. Injection runs when the Rack body responds to to_ary. Non-bufferable streaming bodies are still passed through without calling each. After a successful rewrite, Transfer-Encoding is removed and Content-Length is set to the injected body size.
|
|
10
|
+
|
|
11
|
+
## Effects
|
|
12
|
+
|
|
13
|
+
Pending request-scoped stylesheet tags reach head on bufferable chunked HTML. True streaming responses without to_ary stay untouched. README late-head section matches that contract. Specs cover bufferable chunked inject and non-bufferable pass-through.
|
|
14
|
+
|
|
15
|
+
## Next
|
|
16
|
+
|
|
17
|
+
Shipped in 2.0.2. Confirm consumers that relied on the old skip still expect streaming bodies without to_ary.
|
|
18
|
+
|
|
19
|
+
## Source
|
|
20
|
+
|
|
21
|
+
lib/style_capsule/head_injection_middleware.rb
|
|
22
|
+
spec/style_capsule/head_injection_middleware_spec.rb
|
|
23
|
+
CHANGELOG.md 2.0.2
|
|
24
|
+
README.md Late head injection
|
|
25
|
+
Release 2.0.2.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# json 2.21.1 CVE-2026-71847 in development lockfiles
|
|
2
|
+
|
|
3
|
+
Recorded 2026-09-04 during an engineering and dependency audit of style_capsule 2.0.1. Not a drive-by hunt. bundler-audit reported the advisory on every lockfile this repository audits in CI.
|
|
4
|
+
|
|
5
|
+
## Dependency
|
|
6
|
+
|
|
7
|
+
Name: json. Locked version: 2.21.1. Constraint in this repo: none in the gemspec. The published gem has no runtime gems. Gemfile.lock shows json as a dependency of activesupport 8.1.3.1 and of rubocop 1.88.2 via json (~> 2.3). The same 2.21.1 pin appears in gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby34.gemfile.lock, and gemfiles/rails8ruby4.gemfile.lock.
|
|
8
|
+
|
|
9
|
+
## Symptom
|
|
10
|
+
|
|
11
|
+
bundle-audit check fails. Title: JSON::ResumableParser#partial_value use-after-free crash on truncated duplicate-key streams. CVE-2026-71847. GHSA-9hj4-r449-hfvc. CVSS 8.7 availability. Affected 2.20.0 through 2.21.1. Solution: >= 2.21.2. StyleCapsule source does not parse JSON. A host that only adds this gem does not get json from this gemspec. Developers and CI that install this lockfile do.
|
|
12
|
+
|
|
13
|
+
## Evidence
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
|
|
17
|
+
bundle-audit update then bundle-audit check --no-update on Gemfile.lock.
|
|
18
|
+
|
|
19
|
+
bundle-audit check --no-update --gemfile-lock gemfiles/rails72.gemfile.lock
|
|
20
|
+
|
|
21
|
+
bundle-audit check --no-update --gemfile-lock gemfiles/rails8ruby34.gemfile.lock
|
|
22
|
+
|
|
23
|
+
bundle-audit check --no-update --gemfile-lock gemfiles/rails8ruby4.gemfile.lock
|
|
24
|
+
|
|
25
|
+
Observed: advisory database updated 2026-09-04, commit 478717d. All four locks named json 2.21.1 and the CVE above. bundle outdated --strict listed json 2.21.1 with newest 2.21.2. Rubygems versions API: 2.21.1 created 2026-07-13, 2.21.2 created 2026-07-31. GitHub advisory GHSA-9hj4-r449-hfvc. .github/workflows/dependency-audit.yml runs bundle-audit on every Gemfile.lock on push, pull_request, and weekly cron.
|
|
26
|
+
|
|
27
|
+
## Suggested fix
|
|
28
|
+
|
|
29
|
+
Upgrade. bundle update json in the root Gemfile and in each appraisal gemfile until lockfiles resolve json >= 2.21.2. Do not add json as a direct dependency only to pin it unless Bundler cannot reach 2.21.2 through activesupport and rubocop. No patch or vendor is needed. An upstream report is already published.
|
|
30
|
+
|
|
31
|
+
## Next
|
|
32
|
+
|
|
33
|
+
Lockfiles now pin json 2.21.2. bundle-audit check --no-update is clean on Gemfile.lock, gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby34.gemfile.lock, and gemfiles/rails8ruby4.gemfile.lock. Keep the published gemspec free of json unless a later feature parses JSON on the hot path.
|
|
34
|
+
|
|
35
|
+
## Source
|
|
36
|
+
|
|
37
|
+
ruby-advisory-db via bundler-audit, commit 478717d. GHSA-9hj4-r449-hfvc. CVE-2026-71847. Rubygems json 2.21.2. Gemfile.lock activesupport and rubocop entries. .github/workflows/dependency-audit.yml. Parent issue usr/docs/issues/20260904174500_engineering-and-dependency-audit.md.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Engineering and dependency audit of style_capsule 2.0.1
|
|
2
|
+
|
|
3
|
+
Recorded 2026-09-04. Library gem. No product UI, database, queue, or worker. Pipeline is CSS and HTML in, processor and helpers, class and thread caches, optional file writer, stylesheet registry, optional Rack head injection, HTML out.
|
|
4
|
+
|
|
5
|
+
## Decisions
|
|
6
|
+
|
|
7
|
+
This pass is an audit only. No code change, no lockfile bump, no workflow edit. Rankings follow engineering-audit order: danger, then certainty, then impact, then fix cost. Dependency-audit ran because the request named both. The published gemspec has no runtime gems. json 2.21.1 is a development lockfile pin from activesupport and rubocop, not a consumer runtime of this gem.
|
|
8
|
+
|
|
9
|
+
## Effects
|
|
10
|
+
|
|
11
|
+
Test workflow .github/workflows/test.yml runs on workflow_dispatch and push to main only. Lint and appraisal rspec do not run on pull_request. dependency-audit.yml does run on pull_request and would fail today on json 2.21.1 in Gemfile.lock and gemfiles/*.gemfile.lock.
|
|
12
|
+
|
|
13
|
+
Inline CSS is interpolated into style tags and marked html_safe. Helpers use raw(scoped_css). README documents Theme#scoped_css from stored CSS and shows raw in ERB. README Security section claims XSS prevention via Rails HTML escaping. Capsule id is validated. CSS body is not sanitized for a style-tag break. Specs cover attribute escaping in StandaloneHelper, not a closing style tag in CSS.
|
|
14
|
+
|
|
15
|
+
Non-Rails file stylesheet fallback interpolates option keys and values into a link tag. AssetPath rejects path traversal, quotes, angle brackets, and overlong logical paths. It does not reject a javascript: href scheme or quote characters in option values.
|
|
16
|
+
|
|
17
|
+
StylesheetRegistry @inline_cache and @manifest are process-wide hashes without a mutex. Component CSS cache and helper scope cache cap at 256. File path cache caps at 512 with a mutex. Inline cache only drops entries that have expires_at. Registrations with type proc or no TTL grow for the process lifetime. README claims thread-safe head rendering.
|
|
18
|
+
|
|
19
|
+
HeadInjectionMiddleware buffers a 2xx HTML body to inject before the first closing head tag. Chunked transfer is skipped. Live and SSE caveats are documented. Impact of the buffer under large HTML is inference. No RSS or timing bench this session.
|
|
20
|
+
|
|
21
|
+
CssFileWriter falls back to /tmp/style_capsule, writes CSS, then returns nil so the registry can fall back to inline. Shared-host /tmp collision and symlink follow are inference. Fallback specs exist.
|
|
22
|
+
|
|
23
|
+
README Requirements still say Ruby >= 3.0 and Rails >= 6.0. Gemspec required_ruby_version is >= 3.4. Development Rails gems are >= 7.0, < 9.0. Appraisals cover Rails 7.2 and 8.1 on Ruby 3.4 and 4.0. README badge still uses v=2.0.0. Version constant is 2.0.1.
|
|
24
|
+
|
|
25
|
+
lib/style_capsule/stylesheet_registry.rb is 827 lines. House guidance prefers around 150 with a 300 ceiling. AssetPath and HeadInjectionMiddleware are already split out.
|
|
26
|
+
|
|
27
|
+
CssProcessor walks CSS with regex up to 1MB. A comment claims MAX_CSS_SIZE mitigates ReDoS. ReDoS was not proven. Spike was not run.
|
|
28
|
+
|
|
29
|
+
Dependabot lists npm and docker at directory / with no package.json and no Dockerfile. Bundler updates only directory /. Appraisal lockfiles under gemfiles/ are not a separate Dependabot directory.
|
|
30
|
+
|
|
31
|
+
memory_profiler is a development gemspec dependency. No spec or lib caller was found this session.
|
|
32
|
+
|
|
33
|
+
bundle-audit check --no-update on Gemfile.lock, gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby34.gemfile.lock, and gemfiles/rails8ruby4.gemfile.lock all reported json 2.21.1, CVE-2026-71847, GHSA-9hj4-r449-hfvc, fix >= 2.21.2. Advisory database updated 2026-09-04, commit 478717d. Evidence is in usr/docs/dependencies/20260904174500_json-cve-2026-71847.md.
|
|
34
|
+
|
|
35
|
+
bundle outdated --strict listed erb 6.0.6 to 6.0.7, io-console 0.8.2 to 0.9.2, json 2.21.1 to 2.21.2, polyrun 2.2.2 to 2.2.4, psych 5.4.0 to 5.5.0, rack 3.2.6 to 3.2.7, reline 0.6.3 to 0.7.0, view_component 4.12.0 to 4.15.0, webmock 3.26.2 to 3.26.4, zeitwerk 2.8.2 to 2.8.3. Release-date lag for those three named packages versus latest: json 18 days, polyrun 41 days, view_component 82 days. Full lockfile libyears versus every gem latest date was not computed. rspec was not run.
|
|
36
|
+
|
|
37
|
+
Modes skipped with reason: product-surface, this gem has no own UI. Privacy, the gem does not collect personal data; host CSS may appear in instrumentation payloads when a subscriber exists. Observability, no product runtime or SLO of its own. Learned systems, none. Resource cheaper-or-greener without a bench stays inference. Identity claims without HAR or log fixtures stay inference.
|
|
38
|
+
|
|
39
|
+
A later pass on 2026-09-04 implemented the ranked fixes on branch patch/audit-fixes. See usr/docs/changelogs/20260904180000_audit-fixes.md. bundle exec rspec: 498 examples, 0 failures. bundle exec rubocop and bundle exec rbs validate ran after the cop fix in CssProcessor.reject_style_element_breakout!. This file remains the audit record.
|
|
40
|
+
|
|
41
|
+
## Next
|
|
42
|
+
|
|
43
|
+
stylesheet_registry.rb remains over the house line-count ceiling. Split only if a later change owns that file. json lockfile bump is tracked in usr/docs/dependencies/20260904174500_json-cve-2026-71847.md.
|
|
44
|
+
|
|
45
|
+
## Source
|
|
46
|
+
|
|
47
|
+
engineering-audit and dependency-audit skills in .agents/skills. README Requirements and Security. style_capsule.gemspec. lib/style_capsule/version.rb. lib/style_capsule/stylesheet_registry.rb. lib/style_capsule/helper.rb. lib/style_capsule/standalone_helper.rb. lib/style_capsule/css_file_writer.rb. lib/style_capsule/head_injection_middleware.rb. .github/workflows/test.yml. .github/workflows/dependency-audit.yml. .github/dependabot.yml. Gemfile.lock and gemfiles/*.gemfile.lock. bundle-audit 0.9.x against ruby-advisory-db commit 478717d. bundle outdated --strict. ruby -Ilib require of StyleCapsule::ClassRegistry. Rubygems version API dates for json, polyrun, and view_component. Downstream canvas is outside this repo.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
CI quality failed on a trailing blank line in spec/support/junit_formatter.rb. Test CI also ran bundler-audit or osv-scanner, which fail whenever an advisory lands even when a separate dependency audit already exists.
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
- amkisko
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
- Drop bundler-audit and security jobs from test.yml so tests no longer wait on advisory freshness.
|
|
10
|
+
- Keep dependency-audit.yml as workflow_dispatch only. Do not run it on push, pull request, or schedule.
|
|
11
|
+
- Disable osv-scanner in Trunk Check.
|
|
12
|
+
- Disable markdownlint MD013 in .markdownlint.yaml. Keep AGENTS.md in Trunk so other markdownlint rules still run.
|
|
13
|
+
- Keep lockfile and spec fixes that were already blocking quality or tests.
|
|
14
|
+
|
|
15
|
+
## Effects
|
|
16
|
+
|
|
17
|
+
- Test CI runs lint and specs without an advisory gate.
|
|
18
|
+
- Trunk Check no longer treats dependency CVEs as a required check.
|
|
19
|
+
- Manual dependency-audit remains available on workflow_dispatch.
|
|
20
|
+
|
|
21
|
+
## Next
|
|
22
|
+
|
|
23
|
+
- Run dependency audit on demand when a release or a known advisory needs it.
|
|
24
|
+
- Do not reintroduce advisory scanners into test.yml.
|
|
25
|
+
|
|
26
|
+
## Source
|
|
27
|
+
|
|
28
|
+
- GitHub Actions test and Trunk Check failures on 2026-09-17
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "digest/sha1"
|
|
5
|
+
require "tmpdir"
|
|
5
6
|
require_relative "instrumentation"
|
|
6
7
|
|
|
7
8
|
module StyleCapsule
|
|
@@ -12,7 +13,7 @@ module StyleCapsule
|
|
|
12
13
|
# via Rails asset pipeline.
|
|
13
14
|
#
|
|
14
15
|
# In production environments where the app directory is read-only (e.g., Docker containers),
|
|
15
|
-
# this class automatically falls back to writing files
|
|
16
|
+
# this class automatically falls back to writing files under Dir.tmpdir when the
|
|
16
17
|
# default location is not writable. When using the fallback directory, write_css returns
|
|
17
18
|
# nil, causing StylesheetRegistry to fall back to inline CSS (keeping the UI functional).
|
|
18
19
|
#
|
|
@@ -32,7 +33,7 @@ module StyleCapsule
|
|
|
32
33
|
# StyleCapsule::CssFileWriter.configure(
|
|
33
34
|
# output_dir: Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR),
|
|
34
35
|
# filename_pattern: ->(component_class, capsule_id) { "capsule-#{capsule_id}.css" },
|
|
35
|
-
# fallback_dir:
|
|
36
|
+
# fallback_dir: StyleCapsule::CssFileWriter::FALLBACK_OUTPUT_DIR
|
|
36
37
|
# )
|
|
37
38
|
#
|
|
38
39
|
# @example Usage
|
|
@@ -74,7 +75,7 @@ module StyleCapsule
|
|
|
74
75
|
# Default output directory for CSS files (relative to Rails root)
|
|
75
76
|
DEFAULT_OUTPUT_DIR = "app/assets/builds/capsules"
|
|
76
77
|
# Fallback directory for when default location is read-only (absolute path)
|
|
77
|
-
FALLBACK_OUTPUT_DIR = "
|
|
78
|
+
FALLBACK_OUTPUT_DIR = File.join(Dir.tmpdir, "style_capsule-#{Process.uid}")
|
|
78
79
|
# Positive cache for resolved asset-relative paths (avoids repeated File.exist? on hot path)
|
|
79
80
|
FILE_PATH_CACHE_MAX = 512
|
|
80
81
|
|
|
@@ -90,7 +91,7 @@ module StyleCapsule
|
|
|
90
91
|
# Default: `"capsule-#{capsule_id}.css"` (capsule_id is unique and deterministic)
|
|
91
92
|
# @param enabled [Boolean] Whether file writing is enabled (default: true)
|
|
92
93
|
# @param fallback_dir [String, Pathname, nil] Fallback directory when default location is read-only
|
|
93
|
-
# Default: `StyleCapsule::CssFileWriter::FALLBACK_OUTPUT_DIR`
|
|
94
|
+
# Default: `StyleCapsule::CssFileWriter::FALLBACK_OUTPUT_DIR`
|
|
94
95
|
# @example
|
|
95
96
|
# StyleCapsule::CssFileWriter.configure(
|
|
96
97
|
# output_dir: Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR),
|
|
@@ -49,7 +49,7 @@ module StyleCapsule
|
|
|
49
49
|
raise ArgumentError, "CSS content exceeds maximum size of #{MAX_CSS_SIZE} bytes (got #{css_string.bytesize} bytes)"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
reject_style_element_breakout!(css_string)
|
|
53
53
|
validate_capsule_id!(capsule_id)
|
|
54
54
|
|
|
55
55
|
# Instrument CSS processing with timing and size metrics
|
|
@@ -142,7 +142,7 @@ module StyleCapsule
|
|
|
142
142
|
raise ArgumentError, "CSS content exceeds maximum size of #{MAX_CSS_SIZE} bytes (got #{css_string.bytesize} bytes)"
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
reject_style_element_breakout!(css_string)
|
|
146
146
|
validate_capsule_id!(capsule_id)
|
|
147
147
|
|
|
148
148
|
# Instrument CSS processing with timing and size metrics
|
|
@@ -169,6 +169,20 @@ module StyleCapsule
|
|
|
169
169
|
css.gsub(/\/\*.*?\*\//m, "")
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
+
# Reject CSS that would close an enclosing HTML style element
|
|
173
|
+
#
|
|
174
|
+
# HTML parses style-element content until the first case-insensitive closer,
|
|
175
|
+
# including sequences inside CSS comments or string literals.
|
|
176
|
+
#
|
|
177
|
+
# @param css_string [String, nil] CSS that will be placed inside a style tag
|
|
178
|
+
# @raise [ArgumentError] If the string contains a style element closer
|
|
179
|
+
def self.reject_style_element_breakout!(css_string)
|
|
180
|
+
return if css_string.blank?
|
|
181
|
+
return unless css_string.match?(%r{</style}i)
|
|
182
|
+
|
|
183
|
+
raise ArgumentError, "CSS content must not contain a style element closer"
|
|
184
|
+
end
|
|
185
|
+
|
|
172
186
|
# Validate capsule ID to prevent injection attacks
|
|
173
187
|
#
|
|
174
188
|
# @param capsule_id [String] Capsule ID to validate
|
|
@@ -32,8 +32,11 @@ module StyleCapsule
|
|
|
32
32
|
injected_body = StylesheetRegistry.inject_pending_head_stylesheets(body, view_context)
|
|
33
33
|
return [status, headers, response] if injected_body.equal?(body)
|
|
34
34
|
|
|
35
|
+
close_response(response)
|
|
36
|
+
|
|
35
37
|
headers = headers.dup
|
|
36
|
-
headers
|
|
38
|
+
headers.delete("Transfer-Encoding")
|
|
39
|
+
headers["Content-Length"] = injected_body.bytesize.to_s
|
|
37
40
|
|
|
38
41
|
[status, headers, [injected_body]]
|
|
39
42
|
end
|
|
@@ -41,15 +44,20 @@ module StyleCapsule
|
|
|
41
44
|
def inject_response?(status, headers, response)
|
|
42
45
|
return false unless SUCCESS_STATUS.cover?(status)
|
|
43
46
|
return false unless html_content_type?(headers["Content-Type"])
|
|
44
|
-
return false
|
|
45
|
-
return false if
|
|
47
|
+
return false unless bufferable_body?(response)
|
|
48
|
+
return false if empty_body?(response)
|
|
46
49
|
|
|
47
50
|
true
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
# Rack 3: +to_ary+ means the body may be buffered without changing application behaviour.
|
|
54
|
+
# Streaming / Live bodies omit +to_ary+; leave those alone even when Transfer-Encoding is chunked.
|
|
55
|
+
def bufferable_body?(response)
|
|
56
|
+
response.respond_to?(:to_ary)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def empty_body?(response)
|
|
60
|
+
response.to_ary == [""]
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def html_content_type?(content_type)
|
|
@@ -64,6 +72,10 @@ module StyleCapsule
|
|
|
64
72
|
parts.join
|
|
65
73
|
end
|
|
66
74
|
|
|
75
|
+
def close_response(response)
|
|
76
|
+
response.close if response.respond_to?(:close)
|
|
77
|
+
end
|
|
78
|
+
|
|
67
79
|
def view_context_for(env)
|
|
68
80
|
controller = env["action_controller.instance"]
|
|
69
81
|
controller&.view_context
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cgi"
|
|
3
4
|
require_relative "instrumentation"
|
|
4
5
|
require_relative "asset_path"
|
|
6
|
+
require_relative "css_processor"
|
|
5
7
|
|
|
6
8
|
module StyleCapsule
|
|
7
9
|
# Helper to determine the parent class for StylesheetRegistry
|
|
@@ -72,14 +74,18 @@ module StyleCapsule
|
|
|
72
74
|
class StylesheetRegistry < StyleCapsule.stylesheet_registry_parent_class
|
|
73
75
|
# Default namespace for backward compatibility
|
|
74
76
|
DEFAULT_NAMESPACE = :default
|
|
77
|
+
MAX_INLINE_CACHE_ENTRIES = 256
|
|
78
|
+
FALLBACK_LINK_OPTION_NAME = /\A(?:media|title|crossorigin|integrity|nonce|type|id|class|data-[a-zA-Z0-9_-]+)\z/
|
|
75
79
|
|
|
76
80
|
# Process-wide manifest for static file paths (like Propshaft)
|
|
77
81
|
# Organized by namespace: { namespace => { logical_path => { file_path:, options: } } }
|
|
78
82
|
@manifest = {} # rubocop:disable Style/ClassVars, ThreadSafety/MutableClassInstanceVariable
|
|
83
|
+
@manifest_mutex = Mutex.new
|
|
79
84
|
|
|
80
85
|
# Process-wide cache for inline CSS (with expiration support)
|
|
81
86
|
# Structure: { cache_key => { css_content: String, cached_at: Time, expires_at: Time } }
|
|
82
87
|
@inline_cache = {} # rubocop:disable Style/ClassVars, ThreadSafety/MutableClassInstanceVariable
|
|
88
|
+
@inline_cache_mutex = Mutex.new
|
|
83
89
|
|
|
84
90
|
# Track last cleanup time for lazy cleanup (prevents excessive cleanup calls)
|
|
85
91
|
@last_cleanup_time = nil # rubocop:disable Style/ClassVars
|
|
@@ -222,8 +228,10 @@ module StyleCapsule
|
|
|
222
228
|
inline_size: nil,
|
|
223
229
|
cache_strategy: :none
|
|
224
230
|
) do
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
with_manifest do
|
|
232
|
+
@manifest[ns] ||= {}
|
|
233
|
+
@manifest[ns][path] = {file_path: path, options: options}
|
|
234
|
+
end
|
|
227
235
|
end
|
|
228
236
|
end
|
|
229
237
|
|
|
@@ -350,11 +358,10 @@ module StyleCapsule
|
|
|
350
358
|
# @param namespace [Symbol] Namespace (for proc strategy)
|
|
351
359
|
# @return [String, nil] Cached CSS content or nil if not cached/expired
|
|
352
360
|
def self.cached_inline(cache_key, cache_strategy:, cache_ttl: nil, cache_proc: nil, css_content: nil, capsule_id: nil, namespace: nil)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
cached_entry = @inline_cache[cache_key]
|
|
361
|
+
cached_entry = with_inline_cache do
|
|
362
|
+
cleanup_expired_cache_unlocked_if_needed
|
|
363
|
+
@inline_cache[cache_key]
|
|
364
|
+
end
|
|
358
365
|
return nil unless cached_entry
|
|
359
366
|
|
|
360
367
|
# Check expiration based on strategy
|
|
@@ -402,11 +409,16 @@ module StyleCapsule
|
|
|
402
409
|
end
|
|
403
410
|
end
|
|
404
411
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
412
|
+
with_inline_cache do
|
|
413
|
+
unless @inline_cache.key?(cache_key)
|
|
414
|
+
@inline_cache.shift while @inline_cache.size >= MAX_INLINE_CACHE_ENTRIES
|
|
415
|
+
end
|
|
416
|
+
@inline_cache[cache_key] = {
|
|
417
|
+
css_content: css_content,
|
|
418
|
+
cached_at: current_time,
|
|
419
|
+
expires_at: expires_at
|
|
420
|
+
}
|
|
421
|
+
end
|
|
410
422
|
end
|
|
411
423
|
|
|
412
424
|
# Clear inline CSS cache
|
|
@@ -414,10 +426,12 @@ module StyleCapsule
|
|
|
414
426
|
# @param cache_key [String, nil] Specific cache key to clear (nil clears all)
|
|
415
427
|
# @return [void]
|
|
416
428
|
def self.clear_inline_cache(cache_key = nil)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
429
|
+
with_inline_cache do
|
|
430
|
+
if cache_key
|
|
431
|
+
@inline_cache.delete(cache_key)
|
|
432
|
+
else
|
|
433
|
+
@inline_cache.clear
|
|
434
|
+
end
|
|
421
435
|
end
|
|
422
436
|
end
|
|
423
437
|
|
|
@@ -437,13 +451,16 @@ module StyleCapsule
|
|
|
437
451
|
# # In a scheduled job or initializer
|
|
438
452
|
# StyleCapsule::StylesheetRegistry.cleanup_expired_cache
|
|
439
453
|
def self.cleanup_expired_cache
|
|
454
|
+
with_inline_cache { cleanup_expired_cache_unlocked }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def self.cleanup_expired_cache_unlocked
|
|
440
458
|
return 0 if @inline_cache.empty?
|
|
441
459
|
|
|
442
460
|
now = current_time
|
|
443
461
|
expired_keys = []
|
|
444
462
|
|
|
445
463
|
@inline_cache.each do |cache_key, entry|
|
|
446
|
-
# Remove entries that have an expires_at time and it's in the past
|
|
447
464
|
if entry[:expires_at] && now > entry[:expires_at]
|
|
448
465
|
expired_keys << cache_key
|
|
449
466
|
end
|
|
@@ -455,23 +472,24 @@ module StyleCapsule
|
|
|
455
472
|
expired_keys.size
|
|
456
473
|
end
|
|
457
474
|
|
|
458
|
-
|
|
459
|
-
#
|
|
460
|
-
# Only performs cleanup if it's been more than 5 minutes since the last cleanup.
|
|
461
|
-
# This prevents excessive cleanup calls while still preventing memory leaks.
|
|
462
|
-
#
|
|
463
|
-
# @return [void]
|
|
464
|
-
# @api private
|
|
465
|
-
def self.cleanup_expired_cache_if_needed
|
|
466
|
-
# Cleanup every 5 minutes (300 seconds) to balance memory usage and performance
|
|
475
|
+
def self.cleanup_expired_cache_unlocked_if_needed
|
|
467
476
|
cleanup_interval = 300
|
|
468
477
|
|
|
469
478
|
if @last_cleanup_time.nil? || (current_time - @last_cleanup_time) > cleanup_interval
|
|
470
|
-
|
|
479
|
+
cleanup_expired_cache_unlocked
|
|
471
480
|
end
|
|
472
481
|
end
|
|
473
482
|
|
|
474
|
-
|
|
483
|
+
def self.with_inline_cache
|
|
484
|
+
@inline_cache_mutex.synchronize { yield }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def self.with_manifest
|
|
488
|
+
@manifest_mutex.synchronize { yield }
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
private_class_method :cleanup_expired_cache_unlocked, :cleanup_expired_cache_unlocked_if_needed,
|
|
492
|
+
:with_inline_cache, :with_manifest
|
|
475
493
|
|
|
476
494
|
# Get all registered file paths from process-wide manifest (organized by namespace)
|
|
477
495
|
#
|
|
@@ -540,11 +558,13 @@ module StyleCapsule
|
|
|
540
558
|
# @param namespace [Symbol, String, nil] Optional namespace to clear (nil clears all)
|
|
541
559
|
# @return [void]
|
|
542
560
|
def self.clear_manifest(namespace: nil)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
561
|
+
with_manifest do
|
|
562
|
+
if namespace.nil?
|
|
563
|
+
@manifest = {}
|
|
564
|
+
else
|
|
565
|
+
ns = normalize_namespace(namespace)
|
|
566
|
+
@manifest.delete(ns)
|
|
567
|
+
end
|
|
548
568
|
end
|
|
549
569
|
end
|
|
550
570
|
|
|
@@ -564,7 +584,17 @@ module StyleCapsule
|
|
|
564
584
|
# If nil, falls back to basic HTML generation
|
|
565
585
|
# @param namespace [Symbol, String, nil] Optional namespace to render (nil/blank renders all namespaces)
|
|
566
586
|
# @return [String] HTML-safe string with stylesheet tags
|
|
567
|
-
|
|
587
|
+
def self.render_stylesheet_entries(stylesheets, view_context)
|
|
588
|
+
stylesheets.map do |stylesheet|
|
|
589
|
+
if stylesheet[:type] == :inline
|
|
590
|
+
render_inline_stylesheet(stylesheet, view_context)
|
|
591
|
+
else
|
|
592
|
+
render_file_stylesheet(stylesheet, view_context)
|
|
593
|
+
end
|
|
594
|
+
end.join("\n").then { |s| safe_string(s) }
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- renders mixed inline and file registrations
|
|
568
598
|
def self.render_head_stylesheets(view_context = nil, namespace: nil)
|
|
569
599
|
if namespace.nil? || namespace.to_s.strip.empty?
|
|
570
600
|
request_files_snapshot = request_file_stylesheets.transform_values { |files| files.keys.dup }
|
|
@@ -578,16 +608,9 @@ module StyleCapsule
|
|
|
578
608
|
|
|
579
609
|
return safe_string("") if all_stylesheets.empty?
|
|
580
610
|
|
|
581
|
-
rendered = all_stylesheets
|
|
582
|
-
if stylesheet[:type] == :inline
|
|
583
|
-
render_inline_stylesheet(stylesheet, view_context)
|
|
584
|
-
else
|
|
585
|
-
render_file_stylesheet(stylesheet, view_context)
|
|
586
|
-
end
|
|
587
|
-
end.join("\n").then { |s| safe_string(s) }
|
|
611
|
+
rendered = render_stylesheet_entries(all_stylesheets, view_context)
|
|
588
612
|
|
|
589
613
|
clear_snapshotted_request_registrations(request_files_snapshot, request_inline_snapshot)
|
|
590
|
-
rendered
|
|
591
614
|
|
|
592
615
|
else
|
|
593
616
|
# Render specific namespace
|
|
@@ -598,24 +621,19 @@ module StyleCapsule
|
|
|
598
621
|
|
|
599
622
|
return safe_string("") if stylesheets.empty?
|
|
600
623
|
|
|
601
|
-
rendered = stylesheets
|
|
602
|
-
if stylesheet[:type] == :inline
|
|
603
|
-
render_inline_stylesheet(stylesheet, view_context)
|
|
604
|
-
else
|
|
605
|
-
render_file_stylesheet(stylesheet, view_context)
|
|
606
|
-
end
|
|
607
|
-
end.join("\n").then { |s| safe_string(s) }
|
|
624
|
+
rendered = render_stylesheet_entries(stylesheets, view_context)
|
|
608
625
|
|
|
609
626
|
clear_snapshotted_request_registrations_for_namespace(
|
|
610
627
|
ns,
|
|
611
628
|
file_paths: snapshotted_file_paths,
|
|
612
629
|
snapshotted_inline_stylesheets: snapshotted_inline
|
|
613
630
|
)
|
|
614
|
-
rendered
|
|
615
631
|
|
|
616
632
|
end
|
|
633
|
+
|
|
634
|
+
rendered
|
|
617
635
|
end
|
|
618
|
-
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
636
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
619
637
|
|
|
620
638
|
# Check if there are any registered stylesheets
|
|
621
639
|
#
|
|
@@ -668,6 +686,7 @@ module StyleCapsule
|
|
|
668
686
|
end
|
|
669
687
|
|
|
670
688
|
# @api private
|
|
689
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- clears snapshotted request registries per namespace
|
|
671
690
|
def self.clear_snapshotted_request_registrations_for_namespace(namespace, file_paths:, snapshotted_inline_stylesheets:)
|
|
672
691
|
ns = normalize_namespace(namespace)
|
|
673
692
|
|
|
@@ -694,6 +713,7 @@ module StyleCapsule
|
|
|
694
713
|
|
|
695
714
|
self.inline_stylesheets = inline_registry
|
|
696
715
|
end
|
|
716
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
697
717
|
|
|
698
718
|
# Inject pending request-scoped stylesheets into an HTML document before +</head>+.
|
|
699
719
|
#
|
|
@@ -793,20 +813,24 @@ module StyleCapsule
|
|
|
793
813
|
else
|
|
794
814
|
# Fallback if no view context (logical path validated in .register)
|
|
795
815
|
href = "/assets/#{file_path}.css"
|
|
796
|
-
tag_options = options
|
|
816
|
+
tag_options = fallback_link_option_attributes(options)
|
|
797
817
|
safe_string(%(<link rel="stylesheet" href="#{href}"#{" #{tag_options}" unless tag_options.empty?}>))
|
|
798
818
|
end
|
|
799
819
|
end
|
|
800
820
|
|
|
821
|
+
def self.fallback_link_option_attributes(options)
|
|
822
|
+
options.filter_map do |key, value|
|
|
823
|
+
name = key.to_s
|
|
824
|
+
next unless name.match?(FALLBACK_LINK_OPTION_NAME)
|
|
825
|
+
|
|
826
|
+
%(#{name}="#{CGI.escapeHTML(value.to_s)}")
|
|
827
|
+
end.join(" ")
|
|
828
|
+
end
|
|
829
|
+
|
|
801
830
|
# Render an inline stylesheet
|
|
802
|
-
def self.render_inline_stylesheet(stylesheet,
|
|
803
|
-
# CSS content is already scoped when registered from components
|
|
804
|
-
# capsule_id is stored for reference but CSS is pre-processed
|
|
831
|
+
def self.render_inline_stylesheet(stylesheet, _view_context)
|
|
805
832
|
css_content = stylesheet[:css_content]
|
|
806
|
-
|
|
807
|
-
# Construct HTML manually to avoid any HTML escaping issues
|
|
808
|
-
# CSS content should not be HTML-escaped as it's inside a <style> tag
|
|
809
|
-
# Using string interpolation with html_safe ensures CSS is not escaped
|
|
833
|
+
CssProcessor.reject_style_element_breakout!(css_content)
|
|
810
834
|
safe_string(%(<style type="text/css">#{css_content}</style>))
|
|
811
835
|
end
|
|
812
836
|
|
|
@@ -822,6 +846,7 @@ module StyleCapsule
|
|
|
822
846
|
end
|
|
823
847
|
end
|
|
824
848
|
|
|
825
|
-
private_class_method :render_file_stylesheet, :render_inline_stylesheet, :safe_string
|
|
849
|
+
private_class_method :render_file_stylesheet, :render_inline_stylesheet, :safe_string,
|
|
850
|
+
:fallback_link_option_attributes
|
|
826
851
|
end
|
|
827
852
|
end
|
data/sig/style_capsule.rbs
CHANGED
|
@@ -9,6 +9,7 @@ module StyleCapsule
|
|
|
9
9
|
def self.scope_selectors: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
10
10
|
def self.scope_with_nesting: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
11
11
|
def self.strip_comments: (String css) -> String
|
|
12
|
+
def self.reject_style_element_breakout!: (String | nil css_string) -> void
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
class CssFileWriter
|
|
@@ -24,6 +25,7 @@ module StyleCapsule
|
|
|
24
25
|
# At runtime inherits Object when ActiveSupport::CurrentAttributes is unavailable.
|
|
25
26
|
class StylesheetRegistry
|
|
26
27
|
DEFAULT_NAMESPACE: Symbol
|
|
28
|
+
MAX_INLINE_CACHE_ENTRIES: Integer
|
|
27
29
|
|
|
28
30
|
def self.normalize_namespace: (Symbol | String | nil namespace) -> Symbol
|
|
29
31
|
def self.register: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: style_capsule
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '7.0'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '9.0'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '
|
|
28
|
+
version: '7.0'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
@@ -35,7 +35,7 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - ">="
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: '
|
|
38
|
+
version: '7.0'
|
|
39
39
|
- - "<"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
41
|
version: '9.0'
|
|
@@ -45,7 +45,7 @@ dependencies:
|
|
|
45
45
|
requirements:
|
|
46
46
|
- - ">="
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: '
|
|
48
|
+
version: '7.0'
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '9.0'
|
|
@@ -67,16 +67,16 @@ dependencies:
|
|
|
67
67
|
name: polyrun
|
|
68
68
|
requirement: !ruby/object:Gem::Requirement
|
|
69
69
|
requirements:
|
|
70
|
-
- - "
|
|
70
|
+
- - ">="
|
|
71
71
|
- !ruby/object:Gem::Version
|
|
72
|
-
version:
|
|
72
|
+
version: 2.2.0
|
|
73
73
|
type: :development
|
|
74
74
|
prerelease: false
|
|
75
75
|
version_requirements: !ruby/object:Gem::Requirement
|
|
76
76
|
requirements:
|
|
77
|
-
- - "
|
|
77
|
+
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version:
|
|
79
|
+
version: 2.2.0
|
|
80
80
|
- !ruby/object:Gem::Dependency
|
|
81
81
|
name: webmock
|
|
82
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,48 +105,6 @@ dependencies:
|
|
|
105
105
|
- - "~>"
|
|
106
106
|
- !ruby/object:Gem::Version
|
|
107
107
|
version: '13'
|
|
108
|
-
- !ruby/object:Gem::Dependency
|
|
109
|
-
name: simplecov
|
|
110
|
-
requirement: !ruby/object:Gem::Requirement
|
|
111
|
-
requirements:
|
|
112
|
-
- - "~>"
|
|
113
|
-
- !ruby/object:Gem::Version
|
|
114
|
-
version: '0.22'
|
|
115
|
-
type: :development
|
|
116
|
-
prerelease: false
|
|
117
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
-
requirements:
|
|
119
|
-
- - "~>"
|
|
120
|
-
- !ruby/object:Gem::Version
|
|
121
|
-
version: '0.22'
|
|
122
|
-
- !ruby/object:Gem::Dependency
|
|
123
|
-
name: rspec_junit_formatter
|
|
124
|
-
requirement: !ruby/object:Gem::Requirement
|
|
125
|
-
requirements:
|
|
126
|
-
- - "~>"
|
|
127
|
-
- !ruby/object:Gem::Version
|
|
128
|
-
version: '0.6'
|
|
129
|
-
type: :development
|
|
130
|
-
prerelease: false
|
|
131
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
-
requirements:
|
|
133
|
-
- - "~>"
|
|
134
|
-
- !ruby/object:Gem::Version
|
|
135
|
-
version: '0.6'
|
|
136
|
-
- !ruby/object:Gem::Dependency
|
|
137
|
-
name: simplecov-cobertura
|
|
138
|
-
requirement: !ruby/object:Gem::Requirement
|
|
139
|
-
requirements:
|
|
140
|
-
- - "~>"
|
|
141
|
-
- !ruby/object:Gem::Version
|
|
142
|
-
version: '3'
|
|
143
|
-
type: :development
|
|
144
|
-
prerelease: false
|
|
145
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
146
|
-
requirements:
|
|
147
|
-
- - "~>"
|
|
148
|
-
- !ruby/object:Gem::Version
|
|
149
|
-
version: '3'
|
|
150
108
|
- !ruby/object:Gem::Dependency
|
|
151
109
|
name: standard
|
|
152
110
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -273,20 +231,6 @@ dependencies:
|
|
|
273
231
|
- - "~>"
|
|
274
232
|
- !ruby/object:Gem::Version
|
|
275
233
|
version: '2'
|
|
276
|
-
- !ruby/object:Gem::Dependency
|
|
277
|
-
name: memory_profiler
|
|
278
|
-
requirement: !ruby/object:Gem::Requirement
|
|
279
|
-
requirements:
|
|
280
|
-
- - "~>"
|
|
281
|
-
- !ruby/object:Gem::Version
|
|
282
|
-
version: '1'
|
|
283
|
-
type: :development
|
|
284
|
-
prerelease: false
|
|
285
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
286
|
-
requirements:
|
|
287
|
-
- - "~>"
|
|
288
|
-
- !ruby/object:Gem::Version
|
|
289
|
-
version: '1'
|
|
290
234
|
- !ruby/object:Gem::Dependency
|
|
291
235
|
name: rbs
|
|
292
236
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -344,6 +288,11 @@ files:
|
|
|
344
288
|
- LICENSE.md
|
|
345
289
|
- README.md
|
|
346
290
|
- SECURITY.md
|
|
291
|
+
- docs/changelogs/20260904180000_audit-fixes.md
|
|
292
|
+
- docs/changelogs/20260925094000_head-injection-bufferable-chunked.md
|
|
293
|
+
- docs/dependencies/20260904174500_json-cve-2026-71847.md
|
|
294
|
+
- docs/issues/20260904174500_engineering-and-dependency-audit.md
|
|
295
|
+
- docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md
|
|
347
296
|
- docs/non_rails_support.md
|
|
348
297
|
- lib/style_capsule.rb
|
|
349
298
|
- lib/style_capsule/asset_path.rb
|
|
@@ -383,14 +332,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
383
332
|
requirements:
|
|
384
333
|
- - ">="
|
|
385
334
|
- !ruby/object:Gem::Version
|
|
386
|
-
version: '3.
|
|
335
|
+
version: '3.4'
|
|
387
336
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
388
337
|
requirements:
|
|
389
338
|
- - ">="
|
|
390
339
|
- !ruby/object:Gem::Version
|
|
391
340
|
version: '0'
|
|
392
341
|
requirements: []
|
|
393
|
-
rubygems_version: 4.0.
|
|
342
|
+
rubygems_version: 4.0.6
|
|
394
343
|
specification_version: 4
|
|
395
344
|
summary: Attribute-based CSS scoping for Phlex, ViewComponent, and ERB templates.
|
|
396
345
|
test_files: []
|