reactive_component 0.7.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6897d1bd7689445185a2a06d80b868778d1dca89fa15bf1cbbc81ac31ce2ccb2
4
- data.tar.gz: a285c182b5fea6ab970d8d0f609237f81a98d7724d95c88a8f595dce121b361f
3
+ metadata.gz: 8ef3f09713ddda3cdec03ffd6dac861df951589a1b077504a7ea1323e6ac8d73
4
+ data.tar.gz: a08ef29771672722c29145c51f15a9b159d0b9487d801dec2a045d0a5ebf0127
5
5
  SHA512:
6
- metadata.gz: d8c1a5eaddc568b0c8aa20e88a18e7631633d46d344892d447ed0bbe6b12513b7637c0b00794d19679ef2f3da13d1ba012a1559540f754f911c5b2f2179230f1
7
- data.tar.gz: 1b6ec87280c44844c8b93350d21f255280bce22ed79cf192234c6319f1f30971f5e0947e34433c2ee19a72fde6535f31b3a7ccfe5dac3800a10fdc28e50f72fe
6
+ metadata.gz: e7142557dca9daff0f7f5b9a6034144228d47578c417e1a9e959694f8ec0e62371fe4757bffc16738ef7ed8879f40cffa53b347887a5a1a9cb153ce63f7756fc
7
+ data.tar.gz: 92e76f033d5883773851f94f551499c78072a33624fe016ce579834be04e1cfcad000c0e8ba6009500fa2477c05fa3ca401cc755c061c2d86b736a430d224d46
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.2] - 2026-09-07
4
+
5
+ ### Security
6
+ - The actions endpoint enforces CSRF itself with `protect_from_forgery`
7
+ instead of relying on the host's `load_defaults`.
8
+ - `live_action` tokens expire after `ReactiveComponent.action_token_ttl`
9
+ (default one day). Before, a token stayed valid forever.
10
+ - `request_update` passes only declared `client_state` fields to the
11
+ component. Any other client key was set as an instance variable and
12
+ handed to the constructor.
13
+ - `request_update` over the channel only answers for records whose
14
+ `broadcasts` stream is the one the subscriber verified. Before, any
15
+ subscriber could pull the rendered data of any record id of the model.
16
+ - The channel resolves the component name with `safe_constantize` and
17
+ requires a class that includes `ReactiveComponent`.
18
+
19
+ ### Fixed
20
+ - The renderer controller imports its utils by bare specifier again. The
21
+ 0.7.1 relative import resolved to an undigested `/assets/` URL under
22
+ Propshaft and 404ed, which left Turbo stream sources never connecting and
23
+ every system test failing. The bare specifier works for both importmap
24
+ (`pin_all_from`) and npm (`exports`).
25
+
3
26
  ## [0.7.1] - 2026-09-06
4
27
 
5
28
  ### Added
@@ -7,9 +7,9 @@ module ReactiveComponent
7
7
  class_attribute :filter_callback, default: nil
8
8
 
9
9
  def subscribed
10
- stream_name = verified_stream_name
11
- if stream_name
12
- stream_from stream_name
10
+ @stream_name = verified_stream_name
11
+ if @stream_name
12
+ stream_from @stream_name
13
13
  else
14
14
  reject
15
15
  end
@@ -20,12 +20,8 @@ module ReactiveComponent
20
20
  end
21
21
 
22
22
  def request_update(data)
23
- component_class = data['component'].constantize
24
23
  params = data['params'] || {}
25
-
26
- model_class = component_class.live_model_class
27
- record_id = data['record_id'] || params.delete('record_id')
28
- record = model_class.find_by(id: record_id)
24
+ component_class, record = subscribed_component_and_record(data, params)
29
25
  return unless record
30
26
 
31
27
  if data['record_id'].present?
@@ -35,13 +31,32 @@ module ReactiveComponent
35
31
  transmit({ 'action' => 'remove', 'dom_id' => data['dom_id'] })
36
32
  end
37
33
  else
38
- result = component_class.build_data(record, **params.symbolize_keys)
34
+ client_state = params.slice(*component_class._client_state_fields.keys.map(&:to_s))
35
+ result = component_class.build_data(record, **client_state.symbolize_keys)
39
36
  transmit({ 'action' => 'render', 'data' => result })
40
37
  end
41
38
  end
42
39
 
43
40
  private
44
41
 
42
+ # The component must be a reactive component and the record must broadcast
43
+ # to the stream this subscriber verified: the same stream the wrapper
44
+ # signed into the page. Anything else is a guess at a record id the client
45
+ # was never shown.
46
+ def subscribed_component_and_record(data, params)
47
+ component_class = data['component'].to_s.safe_constantize
48
+ return unless component_class.is_a?(Class) && component_class.include?(ReactiveComponent)
49
+
50
+ record = component_class.live_model_class.find_by(id: data['record_id'] || params.delete('record_id'))
51
+ return unless record
52
+
53
+ stream = ReactiveComponent::Wrapper.find_stream_for(component_class, record)
54
+ signed = Turbo::StreamsChannel.signed_stream_name(stream)
55
+ return unless Turbo::StreamsChannel.verified_stream_name(signed) == @stream_name
56
+
57
+ [component_class, record]
58
+ end
59
+
45
60
  def record_matches?(record, params)
46
61
  return true unless self.class.filter_callback
47
62
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ReactiveComponent
4
4
  class ActionsController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+
5
7
  def create
6
8
  payload = verify_token!.symbolize_keys
7
9
  component_class = payload[:c].constantize
@@ -1,6 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { createConsumer } from "@rails/actioncable"
3
- import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage, duplicateIds, strictData } from "../lib/reactive_renderer_utils.js"
3
+ import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage, duplicateIds, strictData } from "reactive_component/lib/reactive_renderer_utils"
4
4
 
5
5
  const consumer = createConsumer()
6
6
  const log = (...args) => {
@@ -21,6 +21,18 @@ function subscribe(streamValue, controller) {
21
21
  sub = consumer.subscriptions.create(
22
22
  { channel: "ReactiveComponent::Channel", signed_stream_name: streamValue },
23
23
  {
24
+ connected() {
25
+ sub._connected = true
26
+ for (const handler of sub.handlers || []) {
27
+ handler.subscriptionConnected()
28
+ }
29
+ },
30
+ disconnected() {
31
+ sub._connected = false
32
+ for (const handler of sub.handlers || []) {
33
+ handler.subscriptionDisconnected()
34
+ }
35
+ },
24
36
  received: async (message) => {
25
37
  const decoded = message.z ? await decompress(message.z) : message
26
38
  for (const handler of sub.handlers) {
@@ -32,6 +44,7 @@ function subscribe(streamValue, controller) {
32
44
  sub.handlers = new Set()
33
45
  }
34
46
  sub.handlers.add(controller)
47
+ if (sub._connected) controller.subscriptionConnected()
35
48
  }
36
49
 
37
50
  function unsubscribe(streamValue, controller) {
@@ -90,6 +103,14 @@ export default class extends Controller {
90
103
  }
91
104
  }
92
105
 
106
+ subscriptionConnected() {
107
+ this.element.setAttribute("data-reactive-renderer-connected", "")
108
+ }
109
+
110
+ subscriptionDisconnected() {
111
+ this.element.removeAttribute("data-reactive-renderer-connected")
112
+ }
113
+
93
114
  resolveTemplate() {
94
115
  if (this.hasTemplateValue) return this.templateValue
95
116
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactiveComponent
4
- VERSION = '0.7.1'
4
+ VERSION = '0.7.2'
5
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/integer/time'
3
4
  require 'active_support/concern'
4
5
 
5
6
  require_relative 'reactive_component/version'
@@ -14,6 +15,8 @@ module ReactiveComponent
14
15
 
15
16
  mattr_accessor :debug, default: false
16
17
  mattr_accessor :renderer, default: nil
18
+ # How long a live_action token minted into a page stays valid.
19
+ mattr_accessor :action_token_ttl, default: 1.day
17
20
 
18
21
  class Error < StandardError; end
19
22
 
@@ -204,7 +207,7 @@ module ReactiveComponent
204
207
  def live_action_token(record)
205
208
  live_action_verifier.generate(
206
209
  { c: name, m: record.class.name, r: record.id },
207
- purpose: :reactive_component_action
210
+ purpose: :reactive_component_action, expires_in: ReactiveComponent.action_token_ttl
208
211
  )
209
212
  end
210
213
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reactive_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Przemyslaw Lusar