tina4ruby 3.13.153 → 3.13.154

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: ff2b6722d0fadf4d3045e879eed65c3d0a67836ee35ed2d95d95441958e6ac8f
4
- data.tar.gz: f46b4617edc7d85b40612640020e4a7c40c17f1ade00d2ea68090b07a44a7049
3
+ metadata.gz: 22d42b434e0c3d217a6df121286a6f1097d98ddf891cedc552c39857205fde76
4
+ data.tar.gz: 6509d126521868fc0a353f8b855d2ff0bdae2bd6d0338a34ac6dfe8cae71316e
5
5
  SHA512:
6
- metadata.gz: 0c9c47a5e35589686cc8535d010a3116249bb49e8864dec07499308d3ee222a8408a00ad258b7db93df5ac0daf0d3fa30b3871ea91cf23b138c06f9bb602e6e2
7
- data.tar.gz: 5feef0d7194c6c98d2a0d1a17721838628897acd56966915d51cc7c812c89ba147b605be227126f7f1066a595d0b33947ffb108429eb53ffdb4e92b46c093fe5
6
+ metadata.gz: b548d403d07725623371d20143d92406d7a8de3f8121c238050684e10c0eb7e4e5a8721b7d6f93c53239104e12c1556b12116085eb2da70a30797c218c9c7f7f
7
+ data.tar.gz: e8f0a0d68a58c3dc030a960f4f488085ce9af6db6b3543142185874869a083a3190ca6e7a88bd3a80e21c8c517c211f1f9cdb74ffbca776c930755999bd59aea
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ number means the same thing everywhere.
6
6
  **The authoritative release notes for every shipped version live in the documentation:**
7
7
  https://tina4.com/ruby/36-releases
8
8
 
9
+ ## 3.13.154 - 2026-10-23
10
+
11
+ ### BREAKING
12
+ - Realtime (WebRTC): `GET /api/rtc/config` (which mints ephemeral TURN
13
+ credentials) and the `/ws/rtc/{room}` signalling socket now require a valid
14
+ token by default (ADR-0101, tina4-python#252). They were public, with no way
15
+ for an app to close them, so the TURN relay was an open resource. Opt out with
16
+ `TINA4_RTC_REQUIRE_AUTH=false` (or `0`/`no`/`off`) for anonymous calling; any
17
+ other value keeps them secured. If your app relied on the open routes, set
18
+ that env var.
19
+
20
+ ### Fixed
21
+ - Frond: a bare reference to a zero-argument callable global (a zero-arity Proc,
22
+ lambda or Method) is now invoked and its return value used for both `{{ g }}`
23
+ output and `{% if g %}` truthiness, matching Node (ADR-0085,
24
+ tina4-documentation#90). `g()` still calls exactly once; a non-callable global
25
+ and a callable that declares parameters are unchanged.
26
+ - CSP: the per-response nonce is no longer injected into a `script-src` /
27
+ `style-src` directive that already carries `'unsafe-inline'` (ADR-0100,
28
+ tina4-php#330). A browser ignores `'unsafe-inline'` once a nonce joins the same
29
+ directive, so inline content an app deliberately allowed through `TINA4_CSP`
30
+ was silently blocked from 3.13.143 on. The app's explicit choice now wins; the
31
+ strict default policy is still nonced.
32
+
33
+ ### Changed
34
+ - Skills: the maintainer skill gains an off-skill self-audit reflex.
35
+
9
36
  ## 3.13.153 - 2026-10-21
10
37
 
11
38
  ### Fixed
data/lib/tina4/csp.rb CHANGED
@@ -95,9 +95,20 @@ module Tina4
95
95
  %w[style-src script-src].each do |directive|
96
96
  existing = directives.find { |name, _| name == directive }
97
97
  if existing
98
+ # ADR-0100: the app opted this directive into inline via
99
+ # 'unsafe-inline'. A browser ignores 'unsafe-inline' the moment a
100
+ # nonce joins the same directive (CSP Level 2+), so adding the nonce
101
+ # would silently block the inline the app asked for. The app wins.
102
+ next if existing[1].split(/\s+/).include?("'unsafe-inline'")
103
+
98
104
  existing[1] = "#{existing[1]} #{token}".strip unless existing[1].split(/\s+/).include?(token)
99
105
  else
100
106
  base = default_value.empty? ? "'self'" : default_value
107
+ # ADR-0100: a directive derived from an 'unsafe-inline' default-src
108
+ # must not be spawned with a nonce - it would cancel the inline the
109
+ # app allowed through default-src.
110
+ next if base.split(/\s+/).include?("'unsafe-inline'")
111
+
101
112
  directives << [directive, "#{base} #{token}".strip]
102
113
  end
103
114
  end
data/lib/tina4/frond.rb CHANGED
@@ -441,7 +441,35 @@ module Tina4
441
441
  # Same dual-callable semantics as ``add_filter`` — see that method for
442
442
  # the static-facade pattern.
443
443
  def self.add_global(name, value)
444
- @@class_globals[name.to_s] = value
444
+ @@class_globals[name.to_s] = wrap_global(value) # ADR-0085
445
+ end
446
+
447
+ # Wrap a bare zero-argument Proc/lambda/Method so a bare reference to it
448
+ # auto-calls (ADR-0085). Node auto-calls any function reached by a bare
449
+ # reference and uses the return value; Ruby stored the proc instead, so
450
+ # `{% if admin_only %}` with `add_global("admin_only", -> { ... })` was
451
+ # always truthy. A callable that declares parameters is left unwrapped and
452
+ # behaves exactly as before; anything that is not a Proc/Method is untouched
453
+ # (matching Node, which only auto-calls plain functions).
454
+ def self.wrap_global(value)
455
+ if (value.is_a?(Proc) || value.is_a?(Method)) && value.arity.zero?
456
+ AutoGlobal.new(value)
457
+ else
458
+ value
459
+ end
460
+ end
461
+
462
+ # Marker for a zero-argument callable global (see wrap_global). `resolve`
463
+ # invokes it once for a bare reference; `admin_only()` still calls it via
464
+ # the ordinary function-call path (`call` forwards).
465
+ class AutoGlobal
466
+ def initialize(fn)
467
+ @fn = fn
468
+ end
469
+
470
+ def call(*args)
471
+ @fn.call(*args)
472
+ end
445
473
  end
446
474
 
447
475
  # Register a custom filter.
@@ -465,7 +493,7 @@ module Tina4
465
493
  #
466
494
  # Updates this instance's live globals map only.
467
495
  def add_global(name, value)
468
- @globals[name.to_s] = value
496
+ @globals[name.to_s] = self.class.wrap_global(value) # ADR-0085
469
497
  self
470
498
  end
471
499
 
@@ -2311,7 +2339,10 @@ module Tina4
2311
2339
  return nil if value.nil?
2312
2340
  end
2313
2341
 
2314
- value
2342
+ # ADR-0085: a bare reference to a zero-argument callable global uses its
2343
+ # return value. `g()` never reaches here as a marker -- the function-call
2344
+ # path invokes it directly -- so each global is called at most once.
2345
+ value.is_a?(AutoGlobal) ? value.call : value
2315
2346
  end
2316
2347
 
2317
2348
  # -----------------------------------------------------------------------
@@ -114,8 +114,17 @@ module Tina4
114
114
 
115
115
  # ----- route registration -------------------------------------------------
116
116
 
117
+ # ADR-0101: the config route mints TURN credentials and the signalling socket
118
+ # relays calls, so both require a valid token UNLESS the app opts out with
119
+ # TINA4_RTC_REQUIRE_AUTH=false (anonymous calling). Secure-by-default: only an
120
+ # explicit false/0/no/off opens them.
121
+ def rtc_require_auth?
122
+ raw = (ENV["TINA4_RTC_REQUIRE_AUTH"] || "true").strip.downcase
123
+ !%w[false 0 no off].include?(raw)
124
+ end
125
+
117
126
  def register_config(paths, features)
118
- Tina4::Router.get(paths["config"]) do |_request, response|
127
+ route = Tina4::Router.get(paths["config"]) do |_request, response|
119
128
  body = { "backend" => "mesh" }
120
129
  if features.include?("calls")
121
130
  body["iceServers"] = Tina4::Realtime.ice_servers
@@ -128,12 +137,14 @@ module Tina4
128
137
  body["files"] = paths["files"] if features.include?("files")
129
138
  response.json(body, Tina4::HTTP_OK)
130
139
  end
140
+ route.secure if rtc_require_auth? # ADR-0101
131
141
  end
132
142
 
133
143
  def register_calls(paths)
134
144
  # WebRTC signalling relay (mesh). Tina4 never parses the SDP; peers filter
135
- # by `to`. The signalling room is public - the media is E2E between peers.
136
- Tina4::Router.websocket("#{paths['signalling']}/{room}") do |connection, event, data|
145
+ # by `to`. The media is E2E between peers; the signalling room requires auth
146
+ # by default (ADR-0101) unless TINA4_RTC_REQUIRE_AUTH=false.
147
+ route = Tina4::Router.websocket("#{paths['signalling']}/{room}") do |connection, event, data|
137
148
  room = connection.params[:room].to_s
138
149
  next if room.empty?
139
150
 
@@ -145,6 +156,7 @@ module Tina4
145
156
  connection.broadcast_to_room(key, data, exclude_self: true)
146
157
  end
147
158
  end
159
+ route.secure if rtc_require_auth? # ADR-0101
148
160
  end
149
161
 
150
162
  def register_chat(paths)
data/lib/tina4/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.153"
10
+ VERSION = "3.13.154"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.153
4
+ version: 3.13.154
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-21 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3