tina4ruby 3.13.154 → 3.13.156

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: 22d42b434e0c3d217a6df121286a6f1097d98ddf891cedc552c39857205fde76
4
- data.tar.gz: 6509d126521868fc0a353f8b855d2ff0bdae2bd6d0338a34ac6dfe8cae71316e
3
+ metadata.gz: 959299a3502a020f30b1f1d0bfd34394cf79180027386888411be340b7b3a9eb
4
+ data.tar.gz: '0196f348b89c4bca2fc5084ff81dafeb83bab3fb3d409a8b987ce77f1cf6bf5a'
5
5
  SHA512:
6
- metadata.gz: b548d403d07725623371d20143d92406d7a8de3f8121c238050684e10c0eb7e4e5a8721b7d6f93c53239104e12c1556b12116085eb2da70a30797c218c9c7f7f
7
- data.tar.gz: e8f0a0d68a58c3dc030a960f4f488085ce9af6db6b3543142185874869a083a3190ca6e7a88bd3a80e21c8c517c211f1f9cdb74ffbca776c930755999bd59aea
6
+ metadata.gz: eddbce15e602d9ad4f6d22d30f613b9a1b82813126d751cfe75b0d81ccb4e07582fd388c7d5edebfb0ece7958fa821d0eb4177c9a4f87d25e15dfdf308d3203f
7
+ data.tar.gz: a0f90bf03d31b7a4745d691d83333357917e144e6e4fdf5643e43f5fd762bfaa00fbc8a0d78a7a3b1a776fe072fe9a1dc0c06e21e030fa19f7e61be2403fd72b
data/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ 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.156 - 2026-11-01
10
+
11
+ ### Added
12
+ - Kafka queue: `Queue#push(payload, raw: true)` (and `produce(..., raw: true)`)
13
+ now produces the value with NO Tina4 envelope (the full Job hash) and no
14
+ injected `id`, so a non-Tina4 service consuming the topic receives the exact
15
+ top-level schema it expects (tina4-ruby#264). Kafka-only; the file/rabbitmq/
16
+ mongodb backends refuse `raw: true` by name (the envelope is internal
17
+ bookkeeping that pop/retries/dead-lettering depend on). Default unchanged.
18
+
19
+ ### Fixed
20
+ - Kafka queue: the consumer's `auto.offset.reset` was hardcoded to `earliest`,
21
+ so a new consumer group could not opt into `latest`. It now reads
22
+ `TINA4_KAFKA_AUTO_OFFSET_RESET` (then the bare `KAFKA_AUTO_OFFSET_RESET`),
23
+ defaulting to `earliest` so current behaviour is unchanged (tina4-ruby#263).
24
+ rdkafka honours it directly, so `latest` takes real effect on the consume path.
25
+ The dead-letter reader still reads from the earliest offset.
26
+
27
+ ## 3.13.155 - 2026-10-29
28
+
29
+ Version bump for cross-framework parity. No Ruby framework changes this release —
30
+ the 3.13.155 fixes were in tina4-python's scaffolding generator and development
31
+ server (generated sync-ORM handlers, the generated test secret, and a serve test
32
+ port above 65535). Ruby already handled every one of those cases.
33
+
9
34
  ## 3.13.154 - 2026-10-23
10
35
 
11
36
  ### BREAKING
@@ -606,18 +606,20 @@ module Tina4
606
606
 
607
607
  # CsrfMiddleware -- validates form tokens on state-changing requests.
608
608
  #
609
- # OFF by default: the middleware is NOT attached unless TINA4_CSRF is truthy
610
- # (true/1/yes/on -- see .attach_from_env, called once at boot in
609
+ # NOT attached by default: the middleware is NOT attached unless TINA4_CSRF is
610
+ # truthy (true/1/yes/on -- see .attach_from_env, called once at boot in
611
611
  # Tina4.initialize!) OR it is registered explicitly via
612
- # Router.use(CsrfMiddleware). Once attached, TINA4_CSRF=false (or 0/no) is the
613
- # kill switch that disables enforcement again.
612
+ # Router.use(CsrfMiddleware). Once attached it enforces unless the kill switch
613
+ # TINA4_CSRF=false (or 0/no) is set -- so with it attached but TINA4_CSRF
614
+ # unset, enforcement is ON (the attach is the opt-in).
614
615
  #
615
616
  # Behaviour (identical to the Python master's CsrfMiddleware.before_csrf):
616
617
  # - Skips GET, HEAD, OPTIONS (safe methods).
617
- # - Skips a public write route (.no_auth / auth: false -- auth_required
618
- # false). The matched Route is attached to the request before this
619
- # post-match middleware runs, so a genuinely public endpoint (login,
620
- # webhook) is not gated.
618
+ # - Skips a route that opts out with .no_csrf (server-to-server webhooks).
619
+ # .no_auth does NOT skip CSRF (#265 / CSRF-DEC-03): a public form can be
620
+ # open to anonymous visitors AND still protected against cross-site posts.
621
+ # The matched Route is attached to the request before this post-match
622
+ # middleware runs.
621
623
  # - Fails CLOSED: with TINA4_SECRET unset the signing secret resolves to
622
624
  # blank (there is NO built-in default), and a blank HMAC key is publicly
623
625
  # reproducible -- so no token can be trusted and every write is rejected
@@ -645,15 +647,18 @@ module Tina4
645
647
  method = (request.method || "GET").upcase
646
648
  return [request, response] if %w[GET HEAD OPTIONS].include?(method)
647
649
 
648
- # 3. Public write routes (.no_auth / auth: false) skip CSRF -- a
649
- # genuinely public endpoint has no session to protect. The matched
650
- # Route is attached to the request before this post-match middleware
651
- # runs (DispatchPipeline#prepare_route_request); a public write route
652
- # has auth_required == false -- the SAME signal the auth gate reads.
653
- # (Reading request.handler, as this once did, was dead code: the live
654
- # request never carries a handler, so the no_auth skip never fired.)
650
+ # 3. A route that opts OUT of CSRF with .no_csrf skips the check -- the
651
+ # escape hatch for server-to-server endpoints (webhooks, callbacks)
652
+ # that carry no browser session and so have no cross-site-forgery
653
+ # surface. .no_auth no longer skips CSRF (#265 / CSRF-DEC-03): a PUBLIC
654
+ # form must be open to anonymous visitors AND protected against
655
+ # cross-site posts, so a form token is still validated on an anonymous
656
+ # write route. no_auth controls IDENTITY (who), CSRF controls ORIGIN
657
+ # (did our own form send it) -- orthogonal; a webhook opts out of CSRF
658
+ # explicitly. The matched Route is attached to the request before this
659
+ # post-match middleware runs (DispatchPipeline#prepare_route_request).
655
660
  route = request.respond_to?(:route) ? request.route : nil
656
- if route.respond_to?(:auth_required) && route.auth_required == false
661
+ if route.respond_to?(:no_csrf?) && route.no_csrf?
657
662
  return [request, response]
658
663
  end
659
664
 
data/lib/tina4/queue.rb CHANGED
@@ -113,10 +113,14 @@ module Tina4
113
113
  # Push a job onto the queue. Returns the Job.
114
114
  # priority: higher-priority messages are dequeued first (default 0).
115
115
  # delay_seconds: delay before the message becomes available (default 0).
116
- def push(payload, priority: 0, delay_seconds: 0)
116
+ # raw: Kafka-only (tina4-ruby#264) — produce the value with NO Tina4 envelope
117
+ # and no injected id, for a non-Tina4 consumer. The file/rabbitmq/mongodb
118
+ # backends refuse raw: true by name (the envelope is internal bookkeeping
119
+ # pop/retries/dead-lettering depend on). Default false = unchanged.
120
+ def push(payload, priority: 0, delay_seconds: 0, raw: false)
117
121
  available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
118
122
  message = Job.new(topic: @topic, payload: payload, priority: priority, available_at: available_at, queue: self)
119
- @backend.enqueue(message)
123
+ @backend.enqueue(message, raw: raw)
120
124
  message
121
125
  end
122
126
 
@@ -247,10 +251,11 @@ module Tina4
247
251
  end
248
252
 
249
253
  # Produce a message onto a topic. Convenience wrapper around push().
250
- def produce(topic, payload, priority: 0, delay_seconds: 0)
254
+ # raw: Kafka-only, un-enveloped produce — see push() (tina4-ruby#264).
255
+ def produce(topic, payload, priority: 0, delay_seconds: 0, raw: false)
251
256
  available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
252
257
  message = Job.new(topic: topic, payload: payload, priority: priority, available_at: available_at, queue: self)
253
- @backend.enqueue(message)
258
+ @backend.enqueue(message, raw: raw)
254
259
  message
255
260
  end
256
261
 
@@ -6,6 +6,7 @@
6
6
  # file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
7
 
8
8
 
9
+ require "json"
9
10
  require_relative "../parse_json"
10
11
  require_relative "republish_retry_policy"
11
12
 
@@ -36,7 +37,7 @@ module Tina4
36
37
  consumer_config = {
37
38
  "bootstrap.servers" => @brokers,
38
39
  "group.id" => @group_id,
39
- "auto.offset.reset" => "earliest",
40
+ "auto.offset.reset" => self.class.offset_reset,
40
41
  "enable.auto.commit" => "false"
41
42
  }.merge(security)
42
43
  @consumer = Rdkafka::Config.new(consumer_config).consumer
@@ -73,6 +74,23 @@ module Tina4
73
74
  config
74
75
  end
75
76
 
77
+ # Resolve the consumer's auto.offset.reset policy from the environment.
78
+ #
79
+ # Where a NEW consumer group (one with no committed offset) starts reading:
80
+ # "earliest" (the default, unchanged) replays the retained backlog;
81
+ # "latest" skips it. Read from the Tina4-namespaced env var first
82
+ # (TINA4_KAFKA_AUTO_OFFSET_RESET), then the bare librdkafka-convention name
83
+ # (KAFKA_AUTO_OFFSET_RESET) — the same precedence the TLS/SASL keys use. A
84
+ # blank value falls through to the default (tina4-ruby#263). rdkafka honours
85
+ # it directly, so "latest" takes real effect on the consume path.
86
+ #
87
+ # Mirrors tina4_python/queue_backends/kafka_backend.py::_offset_reset.
88
+ def self.offset_reset
89
+ env_value("TINA4_KAFKA_AUTO_OFFSET_RESET") ||
90
+ env_value("KAFKA_AUTO_OFFSET_RESET") ||
91
+ "earliest"
92
+ end
93
+
76
94
  # Read an env var, treating empty/blank values as unset (parity with
77
95
  # Python's `os.environ.get(...) or ...` truthiness).
78
96
  def self.env_value(name)
@@ -83,7 +101,7 @@ module Tina4
83
101
  end
84
102
  private_class_method :env_value
85
103
 
86
- def enqueue(message)
104
+ def enqueue(message, raw: false)
87
105
  if message.priority.to_i > 0
88
106
  raise NotImplementedError,
89
107
  "The kafka queue backend cannot honour push(priority): Kafka has no " \
@@ -100,9 +118,14 @@ module Tina4
100
118
  "the push itself."
101
119
  end
102
120
 
121
+ # raw (tina4-ruby#264): produce the bare payload with NO Tina4 envelope
122
+ # (the full Job hash) so a non-Tina4 consumer reads the schema it expects.
123
+ # The id rides as the record KEY only (for partitioning), never in the value.
124
+ payload = raw ? JSON.generate(message.payload) : message.to_json
125
+
103
126
  @producer.produce(
104
127
  topic: message.topic,
105
- payload: message.to_json,
128
+ payload: payload,
106
129
  key: message.id
107
130
  ).wait
108
131
  end
@@ -68,7 +68,17 @@ module Tina4
68
68
  # configuration onto a backend instance passed directly (legacy path).
69
69
  attr_accessor :visibility_timeout
70
70
 
71
- def enqueue(message)
71
+ def enqueue(message, raw: false)
72
+ if raw
73
+ raise NotImplementedError,
74
+ "The file queue backend cannot honour push(raw: true): raw emits " \
75
+ "the message with no Tina4 envelope, which only a message bus " \
76
+ "consumed by non-Tina4 systems (Kafka) needs. This backend stores " \
77
+ "an internal envelope (id/status/attempts/available_at) that pop, " \
78
+ "the retry lifecycle and dead-lettering depend on, so there is " \
79
+ "nothing to omit. Use the kafka backend for raw external messages."
80
+ end
81
+
72
82
  @mutex.synchronize do
73
83
  topic_dir = topic_path(message.topic)
74
84
  FileUtils.mkdir_p(topic_dir)
@@ -63,7 +63,17 @@ module Tina4
63
63
  300.0
64
64
  end
65
65
 
66
- def enqueue(message)
66
+ def enqueue(message, raw: false)
67
+ if raw
68
+ raise NotImplementedError,
69
+ "The mongodb queue backend cannot honour push(raw: true): raw emits " \
70
+ "the message with no Tina4 envelope, which only a message bus " \
71
+ "consumed by non-Tina4 systems (Kafka) needs. This backend stores " \
72
+ "an internal envelope (payload/priority/attempts) that pop, the " \
73
+ "retry lifecycle and dead-lettering depend on, so there is nothing " \
74
+ "to omit. Use the kafka backend for raw external messages."
75
+ end
76
+
67
77
  # Queue#push already resolved delay_seconds into an available_at (nil when
68
78
  # undelayed). Persisting it is what makes a delayed job invisible until
69
79
  # its time: dequeue filters on it below. Before 3.13.95 this field was
@@ -37,7 +37,17 @@ module Tina4
37
37
  # Queue propagates its own configuration onto the backend after construction.
38
38
  attr_accessor :max_retries
39
39
 
40
- def enqueue(message)
40
+ def enqueue(message, raw: false)
41
+ if raw
42
+ raise NotImplementedError,
43
+ "The rabbitmq queue backend cannot honour push(raw: true): raw emits " \
44
+ "the message with no Tina4 envelope, which only a message bus " \
45
+ "consumed by non-Tina4 systems (Kafka) needs. This backend stores " \
46
+ "an internal envelope (payload/priority/attempts) that pop, the " \
47
+ "retry lifecycle and dead-lettering depend on, so there is nothing " \
48
+ "to omit. Use the kafka backend for raw external messages."
49
+ end
50
+
41
51
  if message.priority.to_i > 0
42
52
  raise NotImplementedError,
43
53
  "The rabbitmq queue backend cannot honour push(priority): RabbitMQ " \
data/lib/tina4/router.rb CHANGED
@@ -32,6 +32,10 @@ module Tina4
32
32
  # PY-10-02. Call .no_auth on the route to opt out explicitly.
33
33
  @auth_required = %w[POST PUT PATCH DELETE].include?(@method)
34
34
  @cached = false
35
+ # CSRF is enforced on a write route even when it is public (#265): a form
36
+ # token proves ORIGIN, not identity. Call .no_csrf to opt a server-to-
37
+ # server endpoint (webhook, callback) out of the CSRF form-token check.
38
+ @no_csrf = false
35
39
  @roles = []
36
40
  @perms = []
37
41
  @param_names = []
@@ -53,6 +57,23 @@ module Tina4
53
57
  self
54
58
  end
55
59
 
60
+ # Opt a route OUT of CSRF form-token validation (webhooks / server-to-server
61
+ # callbacks that carry no browser session). .no_auth no longer skips CSRF
62
+ # (#265): a public form stays CSRF-protected. CSRF (origin) and auth
63
+ # (identity) are orthogonal — pair .no_csrf with .no_auth for a public webhook.
64
+ # Returns self for chaining: Router.post("/webhook") { ... }.no_auth.no_csrf
65
+ def no_csrf
66
+ @no_csrf = true
67
+ self
68
+ end
69
+
70
+ # True when this route opts out of CSRF (set by .no_csrf). Predicate reader,
71
+ # kept distinct from the chainable .no_csrf setter (same pattern as
72
+ # auth_required vs .secure/.no_auth) so neither shadows the other.
73
+ def no_csrf?
74
+ @no_csrf == true
75
+ end
76
+
56
77
  # RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
57
78
  # claim. Stack .role/.can for AND. Implies auth. Feature 138 / ADR-0058.
58
79
  # Returns self for chaining: Router.get("/admin") { ... }.role("admin")
data/lib/tina4/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.154"
10
+ VERSION = "3.13.156"
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.154
4
+ version: 3.13.156
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-23 00:00:00.000000000 Z
11
+ date: 2026-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3