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 +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/tina4/middleware.rb +21 -16
- data/lib/tina4/queue.rb +9 -4
- data/lib/tina4/queue_backends/kafka_backend.rb +26 -3
- data/lib/tina4/queue_backends/lite_backend.rb +11 -1
- data/lib/tina4/queue_backends/mongo_backend.rb +11 -1
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +11 -1
- data/lib/tina4/router.rb +21 -0
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 959299a3502a020f30b1f1d0bfd34394cf79180027386888411be340b7b3a9eb
|
|
4
|
+
data.tar.gz: '0196f348b89c4bca2fc5084ff81dafeb83bab3fb3d409a8b987ce77f1cf6bf5a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/tina4/middleware.rb
CHANGED
|
@@ -606,18 +606,20 @@ module Tina4
|
|
|
606
606
|
|
|
607
607
|
# CsrfMiddleware -- validates form tokens on state-changing requests.
|
|
608
608
|
#
|
|
609
|
-
#
|
|
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
|
|
613
|
-
#
|
|
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
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
#
|
|
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.
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
#
|
|
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?(:
|
|
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
|
-
|
|
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
|
-
|
|
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" =>
|
|
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:
|
|
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
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.
|
|
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-
|
|
11
|
+
date: 2026-11-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|