phlex-reactive 0.9.0 → 0.9.1

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: e71368e7ea5c16983ff2be0b1ce7d33b50118290dd6467da7fccf6794860da65
4
- data.tar.gz: 6a2a5aa30b74f206ed7a451ea64e46427a1552aeebbf1875af614e6b7615190a
3
+ metadata.gz: 0b8404995a25e3917e5785b4e7becaceff75359c5577dd82f8b44e8dd95e2049
4
+ data.tar.gz: f1a6e61d8d840aab2273f95405fc19e3e58295c54d2bf10e40ef21bc9bef2b75
5
5
  SHA512:
6
- metadata.gz: b916d841203cf2cb0901cdf379e25bffb03331c49a4091a7436d1aa921f9d5c3a1dea1a35154e6553819e2b9e097a200bc7ead22b52bc5826444f6bb0fbab8d7
7
- data.tar.gz: 36f9fb6e9772ed3f9e354fc32a9560a6dda70e0a156f1fa8429f4e2d9b9f35f3caba0ae7f43dcb72ee11c7a17157b278051f1ebb5cce58cfe17b4cc39496a003
6
+ metadata.gz: 4218da118f87bb6319b54ec8583437f5d208305d936eedbd81b9eaf01ff17941f5a8b7baac98ef141b29ace3b6dbae937b0cee10368b482b2fef2ba9268c49f4
7
+ data.tar.gz: ead67e058d25a3443286e70f9a245746604cc584de2a0160446e56a306a99c5f95f8ec0536ca0ec5a1fd1205a3c63ecf27d741e3f8eb992396155c06a84bdb58
data/CHANGELOG.md CHANGED
@@ -6,6 +6,58 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-07-03
10
+
11
+ Consolidates everything since 0.2.6; tags 0.2.7–0.4.8 shipped without changelog
12
+ sections, so this section spans the whole run to 0.9.0. The jump from 0.4.8 to
13
+ 0.9.0 signals the Tier-2 API wave (#79–#120) and the run-up to 1.0, leaving 0.9.x
14
+ room for dogfood fixes.
15
+
16
+ ### Upgrading from 0.4.x
17
+
18
+ - **BREAKING — an unknown param-type symbol now raises at boot, not at click
19
+ time (#109).** A `params:` schema declared with a type the library doesn't know
20
+ (a typo like `:strng`, or a type you meant to register) raises
21
+ `Phlex::Reactive::UnknownParamType` when the component class loads — the schema
22
+ is compiled once at declaration. Fix the typo or register the type. Previously a
23
+ bad type symbol slipped through to the request and failed obscurely per click.
24
+ - **Minimum Ruby is now 3.4** (was 3.2). Enabling the `it` block parameter
25
+ requires 3.4, so `required_ruby_version` is `>= 3.4.0`. Stay on the 0.3.x line
26
+ if you need Ruby 3.2/3.3.
27
+ - **`optimistic:` / `loading:` / `disable_with:` are now RESERVED `on(...)`
28
+ keywords** (#98, #99), joining `window:` / `once:` / `outside:` / `throttle:`
29
+ from #80. If an action of yours took a free param literally named `optimistic`,
30
+ `loading`, or `disable_with`, rename it — those names now configure the
31
+ client-side hint/loading behavior instead of passing through as an action param.
32
+ - **`flash_builder` → `stream_builder` rename (#113) — no action needed.** The
33
+ builder does far more than flashes, so it was renamed;
34
+ `Phlex::Reactive.flash_builder` and `reset_flash_builder!` remain **permanent
35
+ aliases** (no deprecation). Listed only for grep-ability if you referenced the
36
+ old names.
37
+ - **The client runtime now ships pre-minified (#148) — zero change for importmap
38
+ consumers.** The engine pins the `.min.js` build (106 KB → 22 KB, ~7.7 KB
39
+ gzipped) with a linked sourcemap, so devtools still shows readable source. If
40
+ you vendor the client file by hand, re-vendor from the minified build (the
41
+ dummy app's `spec/dummy/public/vendor/reactive_controller.js` is that build).
42
+
43
+ ### Security
44
+
45
+ - **The signed-token version (`v`) now fails closed on a malformed value.**
46
+ `upgrade_token` type-guards `v` before comparing it: a non-integer or negative
47
+ `v` (only producible with the signing key, or by operator error — `v` lives
48
+ inside the signed blob) now returns `nil` → `400` through the endpoint's
49
+ `|| raise(InvalidToken)`, instead of a 500 on the `v > TOKEN_VERSION`
50
+ comparison or silently treating a negative `v` as a legacy passthrough. The
51
+ fail-closed-on-rollback contract (a `v` newer than this code understands → 400)
52
+ is unchanged.
53
+ - **The raw-array `js([...])` / `broadcast_js_to([...])` escape hatch now
54
+ re-applies the attribute-name allowlist server-side.** The `JS` builder rejects
55
+ event-handler (`on*`), URL-bearing, and `style` attribute names at build time;
56
+ the raw `[[op, args], …]` form skipped that Ruby-side check and relied on the
57
+ client interpreter alone. `Phlex::Reactive::JS.assert_ops_allowed!` restores
58
+ full server-side parity (defense in depth — the client still enforces it too),
59
+ so a hand-built ops array can't emit an `onclick`/`href`/`style` attr op.
60
+
9
61
  ### Changed
10
62
 
11
63
  - **One inheritance-aware registry behind the Component DSL; `component.rb` split
@@ -1552,7 +1604,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1552
1604
  scaffolds a reactive component (and an RSpec spec when the app uses RSpec),
1553
1605
  state-backed by default or record-backed with `--record`.
1554
1606
 
1555
- [Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.3...HEAD
1607
+ [Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.9.0...HEAD
1608
+ [0.9.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.6...v0.9.0
1609
+ [0.2.6]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.5...v0.2.6
1610
+ [0.2.5]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.4...v0.2.5
1556
1611
  [0.2.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.3...v0.2.4
1557
1612
  [0.2.3]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.2...v0.2.3
1558
1613
  [0.2.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.1...v0.2.2
@@ -44,6 +44,44 @@ module Phlex
44
44
  URL_BEARING_ATTRS = %w[href src srcdoc action formaction xlink:href].freeze
45
45
  EVENT_HANDLER_ATTR = /\Aon/i
46
46
 
47
+ # The attr ops whose args carry a "name" the allowlist must gate. Used to
48
+ # re-validate a RAW [op, args] list (the js([...]) / broadcast_js_to([...])
49
+ # escape hatch) that skips the builder's build-time attr_args check.
50
+ ATTR_NAME_OPS = %w[set_attr remove_attr toggle_attr].freeze
51
+
52
+ # Validate a raw ops list ([[op, args], ...] as passed to js/broadcast_js_to
53
+ # without the builder) against the attribute allowlist, so the escape hatch
54
+ # gets the SAME server-side default-deny as the JS chain (defense in depth;
55
+ # the client also enforces it). Non-attr ops and malformed entries pass
56
+ # through untouched — the client interpreter default-denies unknown ops.
57
+ def self.assert_ops_allowed!(list)
58
+ Array(list).each do |op, args|
59
+ next unless ATTR_NAME_OPS.include?(op.to_s) && args.is_a?(::Hash)
60
+
61
+ name = args["name"] || args[:name]
62
+ assert_allowed_attr(name.to_s) if name
63
+ end
64
+ end
65
+
66
+ # The attribute-name allowlist (issue #96), case-insensitive. Refuses
67
+ # event-handler (on*), URL-bearing, and style attributes. Enforced at build
68
+ # time by the instance builder AND on the raw-list escape hatch — two-sided
69
+ # default-deny with the client interpreter.
70
+ def self.assert_allowed_attr(name)
71
+ lower = name.downcase
72
+ if name.match?(EVENT_HANDLER_ATTR)
73
+ raise ArgumentError,
74
+ "#{self}: attribute #{name.inspect} is an event handler (on*) — refused (XSS). " \
75
+ "Client attr ops target hidden/disabled/open/selected/aria-*/data-* and classes."
76
+ end
77
+ return unless URL_BEARING_ATTRS.include?(lower) || lower == "style"
78
+
79
+ raise ArgumentError,
80
+ "#{self}: attribute #{name.inspect} is refused — URL-bearing attributes " \
81
+ "(#{URL_BEARING_ATTRS.join(", ")}) and `style` can't be set from client ops " \
82
+ "(injection surface). Use classes for styling; target aria-*/data-*/boolean attrs."
83
+ end
84
+
47
85
  # The accumulated [name, args] op pairs, oldest first. Frozen.
48
86
  attr_reader :ops
49
87
 
@@ -169,21 +207,10 @@ module Phlex
169
207
  args.freeze
170
208
  end
171
209
 
172
- # The attribute-name allowlist (issue #96), case-insensitive. Enforced here
173
- # AND in the client interpreter two-sided default-deny.
210
+ # Build-time attr-name allowlist for the instance builder — delegates to the
211
+ # shared class-method check (also used by the raw-list escape hatch).
174
212
  def assert_allowed_attr(name)
175
- lower = name.downcase
176
- if name.match?(EVENT_HANDLER_ATTR)
177
- raise ArgumentError,
178
- "#{self.class}: attribute #{name.inspect} is an event handler (on*) — refused (XSS). " \
179
- "Client attr ops target hidden/disabled/open/selected/aria-*/data-* and classes."
180
- end
181
- return unless URL_BEARING_ATTRS.include?(lower) || lower == "style"
182
-
183
- raise ArgumentError,
184
- "#{self.class}: attribute #{name.inspect} is refused — URL-bearing attributes " \
185
- "(#{URL_BEARING_ATTRS.join(", ")}) and `style` can't be set from client ops " \
186
- "(injection surface). Use classes for styling; target aria-*/data-*/boolean attrs."
213
+ self.class.assert_allowed_attr(name)
187
214
  end
188
215
 
189
216
  # A transition must be exactly [during, from, to] class lists (strings).
@@ -275,6 +275,10 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
275
275
  list = Array(ops)
276
276
  raise ArgumentError, "js(...) got no ops — a dead reactive:js stream" if list.empty?
277
277
 
278
+ # The builder validates attr names at build time; a raw [op, args]
279
+ # list skips that, so re-apply the allowlist here (defense in depth —
280
+ # the client interpreter also refuses on*/URL/style attrs).
281
+ Phlex::Reactive::JS.assert_ops_allowed!(list)
278
282
  list.to_json
279
283
  end
280
284
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.9.0"
5
+ VERSION = "0.9.1"
6
6
  end
7
7
  end
@@ -446,6 +446,10 @@ module Phlex
446
446
  # through the endpoint's existing `|| raise(InvalidToken)` → 400.
447
447
  def upgrade_token(payload)
448
448
  version = payload.fetch("v", 0)
449
+ # "v" is inside the signed blob, so only our own key could produce a
450
+ # malformed one — but fail closed rather than 500 on the comparison
451
+ # (String vs Integer) or silently treat a negative "v" as legacy.
452
+ return nil unless version.is_a?(::Integer) && version >= 0
449
453
  return payload if version == TOKEN_VERSION
450
454
  return nil if version > TOKEN_VERSION
451
455
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson