react_on_rails_pro 16.4.0.rc.8 → 16.4.0.rc.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 717ead6ca9b9c1f7354820f063551a8403cabb9fafeb095bd46f35e3d368a483
4
- data.tar.gz: 5062b202868b9106090becb6dd5f040761a2dd37ec2a1323053e43df111044ed
3
+ metadata.gz: c63303b689f4adb5d1be96c2415522aeb05fd210258535c2a8fe3421414870a9
4
+ data.tar.gz: d0e63c69ac3c6ca63021ed54fc6f8f1fa6c574f0a261e74d6d6b4b9970a05c8a
5
5
  SHA512:
6
- metadata.gz: b2fc8c9ad613776be17f834f8a7c2d7ff07b123e6e703b8dd21ffdc4687ee51d0037b49c813c7f7b9596266bfa15885336b1f6ce9df6530e995c68d58557d745
7
- data.tar.gz: 025cf054e18a200c61ad22199617ce0374889b1f825290ee13952f02bb993532cf8b1738c0df4a7295c4b9bf576782bf42abe04395f9071dd46b0686697c8c5b
6
+ metadata.gz: 920e935410da89b4bbd5951c70e1b4f528e7525bba8f2d27094fbe1ce0676ac17f1277a100d235d6d74186a3414765e40f471de7dd0b7bb7e4faa68d04c61426
7
+ data.tar.gz: a4d980d3b3bdb5ace4a9d18902dbfabb35c2c2c99a3c354e5cbc39bda05853500d466d34c748e612dea4e0f0886849ed255be92082274befbaabc971af6b327e
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.4.0.rc.8)
12
+ react_on_rails (16.4.0.rc.9)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,7 +20,7 @@ PATH
20
20
  PATH
21
21
  remote: .
22
22
  specs:
23
- react_on_rails_pro (16.4.0.rc.8)
23
+ react_on_rails_pro (16.4.0.rc.9)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
@@ -29,7 +29,7 @@ PATH
29
29
  httpx (~> 1.5)
30
30
  jwt (~> 2.7)
31
31
  rainbow
32
- react_on_rails (= 16.4.0.rc.8)
32
+ react_on_rails (= 16.4.0.rc.9)
33
33
 
34
34
  GEM
35
35
  remote: https://rubygems.org/
data/LICENSE_SETUP.md CHANGED
@@ -75,13 +75,16 @@ heroku config:set REACT_ON_RAILS_PRO_LICENSE="your_token"
75
75
  Configure your license token via the `REACT_ON_RAILS_PRO_LICENSE` environment variable.
76
76
  Never commit license tokens to version control.
77
77
 
78
- ## License Validation
78
+ ## License Validation and Signals
79
79
 
80
- The license is validated at multiple points:
80
+ License-related checks and signals occur at multiple points:
81
81
 
82
82
  1. **Ruby Gem**: When Rails application starts
83
83
  2. **Node Renderer**: When the Node renderer process starts
84
- 3. **Browser Package**: Trusts server-side validation (via `railsContext.rorPro`)
84
+ 3. **Browser Package**: Receives Pro-installed signal via `railsContext.rorPro` (not license-valid state)
85
+
86
+ The browser package does not perform independent license validation. A valid paid license is still required for
87
+ production deployments.
85
88
 
86
89
  When no license is present, the application runs in **unlicensed mode**. This is fine for development, testing, and CI/CD. Production deployments should always have a valid paid license.
87
90
 
@@ -148,7 +151,7 @@ The task exits with code 0 on success and code 1 if the license is missing, inva
148
151
  | Field | Type | Description |
149
152
  | ---------------------- | --------------- | --------------------------------------------------- |
150
153
  | `status` | string | `"valid"`, `"expired"`, `"invalid"`, or `"missing"` |
151
- | `organization` | string or null | Organization name from the license |
154
+ | `organization` | string or null | Organization name from the JWT `org` claim |
152
155
  | `plan` | string or null | License plan (`"paid"`, `"startup"`, etc.) |
153
156
  | `expiration` | string or null | ISO 8601 expiration date |
154
157
  | `attribution_required` | boolean | Whether attribution is required |
@@ -302,11 +305,13 @@ The license is a JWT (JSON Web Token) signed with RSA-256, containing:
302
305
  "iat": 1234567890, // Issued at timestamp (REQUIRED)
303
306
  "exp": 1234567890, // Expiration timestamp (REQUIRED)
304
307
  "plan": "paid", // License plan (Optional — only "paid" is valid for production)
305
- "organization": "Your Company", // Organization name (Optional)
308
+ "org": "Your Company", // Organization name (Optional)
306
309
  "iss": "api" // Issuer identifier (Optional, standard JWT claim)
307
310
  }
308
311
  ```
309
312
 
313
+ > Note: The JWT claim is `org`. The verify task output uses the field name `organization` for readability.
314
+
310
315
  ### Security
311
316
 
312
317
  - **Offline validation**: No internet connection required
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "timeout"
5
+
6
+ module ReactOnRailsPro
7
+ class CompressionMiddlewareGuard
8
+ COMPATIBILITY_GUIDE_PATH =
9
+ "https://www.shakacode.com/react-on-rails/docs/building-features/" \
10
+ "streaming-server-rendering/#compression-middleware-compatibility"
11
+ PROBLEMATIC_MIDDLEWARES = %w[Rack::Deflater Rack::Brotli].freeze
12
+ PROBE_TIMEOUT_SECONDS = 1
13
+
14
+ Finding = Struct.new(:middleware_name, :source_location, keyword_init: true)
15
+
16
+ def initialize(middlewares:, logger: nil)
17
+ @middlewares = normalize_middlewares(middlewares)
18
+ @logger = logger
19
+ end
20
+
21
+ def findings
22
+ @findings ||= @middlewares.filter_map do |middleware|
23
+ next unless problematic_middleware?(middleware)
24
+
25
+ condition = middleware_condition(middleware)
26
+ next unless condition.respond_to?(:call)
27
+ next unless destructively_iterates_stream?(condition)
28
+
29
+ Finding.new(
30
+ middleware_name: middleware_name(middleware),
31
+ source_location: source_location_for(condition)
32
+ )
33
+ end
34
+ end
35
+
36
+ def warning_messages(root:)
37
+ findings.map do |finding|
38
+ "[React on Rails Pro] #{finding.middleware_name} has a custom `:if` callback" \
39
+ "#{formatted_source_location(finding, root: root)} that calls `body.each`. " \
40
+ "This is incompatible with streaming SSR/RSC and can deadlock `ActionController::Live` responses. " \
41
+ "Remove the custom `:if`, or guard it with " \
42
+ "`return true unless body.respond_to?(:to_ary)` before iterating. " \
43
+ "See #{COMPATIBILITY_GUIDE_PATH}."
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def normalize_middlewares(middlewares)
50
+ if defined?(ActionDispatch::MiddlewareStack) && middlewares.is_a?(ActionDispatch::MiddlewareStack)
51
+ return middlewares.middlewares
52
+ end
53
+
54
+ Array(middlewares)
55
+ end
56
+
57
+ def problematic_middleware?(middleware)
58
+ PROBLEMATIC_MIDDLEWARES.include?(middleware_name(middleware))
59
+ end
60
+
61
+ def middleware_name(middleware)
62
+ middleware.klass.respond_to?(:name) ? middleware.klass.name : middleware.klass.to_s
63
+ end
64
+
65
+ def middleware_condition(middleware)
66
+ Array(middleware.args).filter_map do |arg|
67
+ next unless arg.is_a?(Hash)
68
+
69
+ arg[:if] || arg["if"]
70
+ end.first
71
+ end
72
+
73
+ def destructively_iterates_stream?(condition)
74
+ probe = StreamingBodyProbe.new
75
+
76
+ Timeout.timeout(PROBE_TIMEOUT_SECONDS) do
77
+ condition.call(probe_env, 200, probe_headers, probe)
78
+ end
79
+ probe.iterated?
80
+ rescue StreamingBodyProbe::BodyIteratedError
81
+ true
82
+ rescue Timeout::Error => e
83
+ return true if probe.iterated?
84
+
85
+ log_probe_failure(condition, e, reason: "timed out after #{PROBE_TIMEOUT_SECONDS}s")
86
+ false
87
+ rescue StandardError => e
88
+ return true if probe.iterated?
89
+
90
+ log_probe_failure(condition, e)
91
+ false
92
+ end
93
+
94
+ # Minimal Rack env used to probe `:if` callbacks.
95
+ # Callbacks that depend on application-specific keys can still raise here;
96
+ # those probe failures are logged at debug level and treated as non-findings.
97
+ # Path-gated callbacks can bypass this probe and yield false negatives.
98
+ def probe_env
99
+ {
100
+ "CONTENT_TYPE" => "text/html; charset=utf-8",
101
+ "HTTP_ACCEPT" => "text/html",
102
+ "REQUEST_METHOD" => "GET",
103
+ "PATH_INFO" => "/__react_on_rails_pro_stream_probe__",
104
+ "HTTP_ACCEPT_ENCODING" => "br, gzip, identity",
105
+ "HTTP_HOST" => "example.test",
106
+ "SERVER_NAME" => "example.test",
107
+ "rack.url_scheme" => "https",
108
+ "rack.errors" => StringIO.new,
109
+ "rack.input" => StringIO.new
110
+ }
111
+ end
112
+
113
+ def probe_headers
114
+ {
115
+ "Content-Type" => "text/html; charset=utf-8"
116
+ }
117
+ end
118
+
119
+ def formatted_source_location(finding, root:)
120
+ return "" unless finding.source_location
121
+
122
+ path, line = finding.source_location
123
+ root_prefix = "#{root}/"
124
+ display_path = path.start_with?(root_prefix) ? path.delete_prefix(root_prefix) : path
125
+
126
+ " (#{display_path}:#{line})"
127
+ end
128
+
129
+ def source_location_for(condition)
130
+ condition.respond_to?(:source_location) ? condition.source_location : nil
131
+ end
132
+
133
+ def log_probe_failure(condition, error, reason: nil)
134
+ return unless @logger.respond_to?(:debug)
135
+
136
+ identifier = source_location_for(condition)&.join(":") || condition.class.name || condition.inspect
137
+ backtrace_hint = error.backtrace&.first
138
+
139
+ @logger.debug do
140
+ message = "[React on Rails Pro] CompressionMiddlewareGuard could not probe `:if` callback " \
141
+ "(#{identifier}): "
142
+ message += "#{reason}: " if reason
143
+ message += "#{error.class}: #{error.message}"
144
+ message += " (#{backtrace_hint})" if backtrace_hint
145
+ message
146
+ end
147
+ end
148
+
149
+ class StreamingBodyProbe
150
+ include Enumerable
151
+
152
+ class BodyIteratedError < StandardError; end
153
+
154
+ def iterated?
155
+ @iterated == true
156
+ end
157
+
158
+ def each
159
+ @iterated = true
160
+ raise BodyIteratedError, "Compression middleware `:if` callback called `body.each` on a streaming body."
161
+ end
162
+ end
163
+ end
164
+ end
@@ -7,10 +7,8 @@ module ReactOnRailsPro
7
7
  LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/"
8
8
  # TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).
9
9
  LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key"
10
- RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze
11
10
  private_constant :LICENSE_URL
12
11
  private_constant :LEGACY_LICENSE_FILE
13
- private_constant :RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS
14
12
 
15
13
  initializer "react_on_rails_pro.routes" do
16
14
  ActionDispatch::Routing::Mapper.include ReactOnRailsPro::Routes
@@ -22,8 +20,8 @@ module ReactOnRailsPro
22
20
  config.after_initialize { ReactOnRailsPro::Engine.log_license_status }
23
21
  end
24
22
 
25
- initializer "react_on_rails_pro.check_rsc_streaming_middleware" do
26
- config.after_initialize { ReactOnRailsPro::Engine.log_rsc_streaming_middleware_warning }
23
+ initializer "react_on_rails_pro.warn_on_problematic_compression_middleware" do
24
+ config.after_initialize { ReactOnRailsPro::Engine.log_problematic_compression_middleware_warnings }
27
25
  end
28
26
 
29
27
  class << self
@@ -46,22 +44,12 @@ module ReactOnRailsPro
46
44
  end
47
45
  end
48
46
 
49
- def log_rsc_streaming_middleware_warning
50
- return unless ReactOnRailsPro.configuration.enable_rsc_support
51
- return if Rails.env.test?
52
-
53
- middleware_names = middleware_stack_names
54
- problematic = RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS & middleware_names
55
- return if problematic.empty?
56
-
57
- route_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
58
- Rails.logger.warn(
59
- "[React on Rails Pro] React Server Components support is enabled and the middleware " \
60
- "stack includes #{problematic.join(', ')}. Compression and other response-transforming " \
61
- "middleware can interfere with ActionController::Live NDJSON streaming. If your " \
62
- "`#{route_path}` payload route is not already exempt, consider bypassing " \
63
- "#{problematic.join(', ')} for that endpoint if you see stalled or corrupted RSC payloads."
64
- )
47
+ def log_problematic_compression_middleware_warnings(logger: Rails.logger,
48
+ middlewares: Rails.application.middleware,
49
+ root: Rails.root)
50
+ CompressionMiddlewareGuard.new(middlewares: middlewares, logger: logger)
51
+ .warning_messages(root: root)
52
+ .each { |message| logger.warn(message) }
65
53
  end
66
54
 
67
55
  private
@@ -129,40 +117,6 @@ module ReactOnRailsPro
129
117
  Rails.logger.info message
130
118
  end
131
119
  end
132
-
133
- def middleware_stack_names
134
- middleware_stack = Rails.application&.middleware
135
- return [] unless middleware_stack
136
-
137
- entries =
138
- if middleware_stack.respond_to?(:middlewares)
139
- middleware_stack.middlewares
140
- elsif middleware_stack.respond_to?(:to_a)
141
- middleware_stack.to_a
142
- else
143
- Array(middleware_stack)
144
- end
145
-
146
- entries.filter_map { |entry| middleware_entry_name(entry) }.uniq
147
- end
148
-
149
- def middleware_entry_name(entry)
150
- candidate =
151
- if entry.respond_to?(:klass) && entry.klass
152
- entry.klass
153
- elsif entry.is_a?(Array)
154
- entry.first
155
- else
156
- entry
157
- end
158
-
159
- case candidate
160
- when Module
161
- candidate.name
162
- else
163
- candidate.to_s.presence
164
- end
165
- end
166
120
  end
167
121
  end
168
122
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.4.0.rc.8"
4
+ VERSION = "16.4.0.rc.9"
5
5
  PROTOCOL_VERSION = "2.0.0"
6
6
  end
@@ -6,6 +6,7 @@ require "react_on_rails"
6
6
  require "react_on_rails_pro/request"
7
7
  require "react_on_rails_pro/version"
8
8
  require "react_on_rails_pro/constants"
9
+ require "react_on_rails_pro/compression_middleware_guard"
9
10
  require "react_on_rails_pro/engine"
10
11
  require "react_on_rails_pro/error"
11
12
  require "react_on_rails_pro/utils"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails_pro
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.4.0.rc.8
4
+ version: 16.4.0.rc.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-11 00:00:00.000000000 Z
11
+ date: 2026-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - '='
130
130
  - !ruby/object:Gem::Version
131
- version: 16.4.0.rc.8
131
+ version: 16.4.0.rc.9
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
- version: 16.4.0.rc.8
138
+ version: 16.4.0.rc.9
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: bundler
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -230,6 +230,7 @@ files:
230
230
  - lib/react_on_rails_pro/assets_precompile.rb
231
231
  - lib/react_on_rails_pro/async_value.rb
232
232
  - lib/react_on_rails_pro/cache.rb
233
+ - lib/react_on_rails_pro/compression_middleware_guard.rb
233
234
  - lib/react_on_rails_pro/concerns/async_rendering.rb
234
235
  - lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb
235
236
  - lib/react_on_rails_pro/concerns/stream.rb