tina4ruby 3.13.137 → 3.13.139
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 +40 -0
- data/COMMERCIAL-LICENSE.md +8 -0
- data/LICENSE +373 -0
- data/LICENSE.txt +373 -21
- data/NOTICE +11 -0
- data/README.md +11 -7
- data/exe/tina4ruby +6 -0
- data/lib/tina4/ai.rb +6 -0
- data/lib/tina4/ai_client.rb +10 -3
- data/lib/tina4/api.rb +74 -8
- data/lib/tina4/auth.rb +108 -26
- data/lib/tina4/auto_crud.rb +83 -27
- data/lib/tina4/background.rb +7 -1
- data/lib/tina4/base64.rb +59 -0
- data/lib/tina4/cache.rb +6 -0
- data/lib/tina4/cache_backends/base_backend.rb +14 -0
- data/lib/tina4/cache_backends/database_backend.rb +6 -0
- data/lib/tina4/cache_backends/file_backend.rb +6 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +6 -0
- data/lib/tina4/cache_backends/memory_backend.rb +6 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +22 -1
- data/lib/tina4/cache_backends/redis_backend.rb +6 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +6 -0
- data/lib/tina4/cache_backends.rb +9 -1
- data/lib/tina4/cli.rb +27 -7
- data/lib/tina4/constants.rb +12 -8
- data/lib/tina4/container.rb +6 -0
- data/lib/tina4/context/chunker.rb +6 -0
- data/lib/tina4/context.rb +13 -5
- data/lib/tina4/cors.rb +66 -10
- data/lib/tina4/crud.rb +78 -9
- data/lib/tina4/database/sqlite3_adapter.rb +9 -1
- data/lib/tina4/database.rb +260 -44
- data/lib/tina4/database_adapter.rb +6 -0
- data/lib/tina4/database_result.rb +6 -0
- data/lib/tina4/database_url.rb +6 -0
- data/lib/tina4/debug.rb +6 -0
- data/lib/tina4/dev.rb +6 -0
- data/lib/tina4/dev_admin.rb +222 -89
- data/lib/tina4/dev_mailbox.rb +6 -1
- data/lib/tina4/dispatch_pipeline.rb +86 -1
- data/lib/tina4/docs.rb +6 -0
- data/lib/tina4/docstore.rb +23 -4
- data/lib/tina4/drivers/arango_graph_driver.rb +8 -1
- data/lib/tina4/drivers/bolt_graph_driver.rb +6 -0
- data/lib/tina4/drivers/firebird_driver.rb +95 -6
- data/lib/tina4/drivers/mongodb_driver.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +54 -39
- data/lib/tina4/drivers/mysql_driver.rb +6 -0
- data/lib/tina4/drivers/odbc_driver.rb +6 -0
- data/lib/tina4/drivers/postgres_driver.rb +35 -8
- data/lib/tina4/drivers/schema_split.rb +6 -0
- data/lib/tina4/drivers/sqlite_driver.rb +16 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +14 -6
- data/lib/tina4/error_overlay.rb +6 -0
- data/lib/tina4/events.rb +6 -0
- data/lib/tina4/feedback.rb +9 -2
- data/lib/tina4/field_types.rb +6 -0
- data/lib/tina4/form_parser.rb +241 -0
- data/lib/tina4/frond.rb +200 -58
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +6 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +6 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +6 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +6 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +6 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +6 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +6 -0
- data/lib/tina4/graph.rb +6 -0
- data/lib/tina4/graphql.rb +134 -35
- data/lib/tina4/health.rb +6 -0
- data/lib/tina4/html_element.rb +6 -0
- data/lib/tina4/http_server.rb +690 -0
- data/lib/tina4/imap_client.rb +352 -0
- data/lib/tina4/import_helper.rb +6 -0
- data/lib/tina4/job.rb +21 -2
- data/lib/tina4/localization.rb +6 -0
- data/lib/tina4/log.rb +6 -0
- data/lib/tina4/mail_socket.rb +124 -0
- data/lib/tina4/mcp.rb +18 -9
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +27 -10
- data/lib/tina4/migration.rb +6 -0
- data/lib/tina4/model_collection.rb +6 -0
- data/lib/tina4/mqtt.rb +9 -2
- data/lib/tina4/mqtt_message.rb +6 -0
- data/lib/tina4/orm.rb +72 -16
- data/lib/tina4/parse_json.rb +25 -0
- data/lib/tina4/plan.rb +9 -2
- data/lib/tina4/point.rb +6 -0
- data/lib/tina4/port_takeover.rb +6 -0
- data/lib/tina4/project_index.rb +6 -0
- data/lib/tina4/public/THIRD-PARTY-NOTICES.md +55 -0
- data/lib/tina4/public/bundled-component-provenance.json +48 -0
- data/lib/tina4/public/js/tina4js.min.js +13 -3
- data/lib/tina4/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- data/lib/tina4/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- data/lib/tina4/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- data/lib/tina4/public/licenses/tina4-css-MIT.txt +21 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-components.json +240 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- data/lib/tina4/public/licenses/tina4-js-MIT.txt +20 -0
- data/lib/tina4/push.rb +22 -4
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +18 -2
- data/lib/tina4/queue_backends/lite_backend.rb +18 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +18 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +18 -2
- data/lib/tina4/rack_app.rb +62 -50
- data/lib/tina4/rate_limiter.rb +6 -0
- data/lib/tina4/realtime/attachment.rb +6 -0
- data/lib/tina4/realtime/channel.rb +6 -0
- data/lib/tina4/realtime/channel_member.rb +6 -0
- data/lib/tina4/realtime/local_storage.rb +6 -0
- data/lib/tina4/realtime/message.rb +6 -0
- data/lib/tina4/realtime/s3_storage.rb +15 -1
- data/lib/tina4/realtime/storage.rb +6 -0
- data/lib/tina4/realtime/storage_backend.rb +6 -0
- data/lib/tina4/realtime/workspace.rb +6 -0
- data/lib/tina4/realtime.rb +10 -3
- data/lib/tina4/request.rb +90 -127
- data/lib/tina4/response.rb +71 -9
- data/lib/tina4/response_cache.rb +6 -0
- data/lib/tina4/router.rb +6 -0
- data/lib/tina4/secret_file.rb +34 -0
- data/lib/tina4/seeder.rb +9 -3
- data/lib/tina4/service.rb +8 -2
- data/lib/tina4/service_runner.rb +6 -0
- data/lib/tina4/session.rb +6 -0
- data/lib/tina4/session_handlers/database_handler.rb +42 -12
- data/lib/tina4/session_handlers/file_handler.rb +6 -0
- data/lib/tina4/session_handlers/memcached_handler.rb +6 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +108 -39
- data/lib/tina4/session_handlers/mongo_wire_client.rb +23 -9
- data/lib/tina4/session_handlers/redis_handler.rb +6 -0
- data/lib/tina4/session_handlers/resp_client.rb +6 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -0
- data/lib/tina4/shutdown.rb +17 -14
- data/lib/tina4/smtp_client.rb +222 -0
- data/lib/tina4/sql_translator.rb +93 -0
- data/lib/tina4/sqlite3_gem.rb +27 -0
- data/lib/tina4/sso.rb +52 -12
- data/lib/tina4/ssrf.rb +148 -0
- data/lib/tina4/swagger.rb +6 -0
- data/lib/tina4/template.rb +6 -0
- data/lib/tina4/test.rb +8 -2
- data/lib/tina4/test_client.rb +8 -1
- data/lib/tina4/testing.rb +8 -1
- data/lib/tina4/validator.rb +6 -0
- data/lib/tina4/version.rb +7 -1
- data/lib/tina4/webserver.rb +25 -143
- data/lib/tina4/websocket.rb +170 -20
- data/lib/tina4/websocket_backplane.rb +25 -2
- data/lib/tina4/wsdl.rb +54 -18
- data/lib/tina4/xml_parser.rb +268 -0
- data/lib/tina4.rb +98 -33
- data/lib/tina4ruby.rb +6 -0
- metadata +44 -140
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
data/lib/tina4/push.rb
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "openssl"
|
|
4
|
-
|
|
10
|
+
require_relative "base64"
|
|
5
11
|
require "json"
|
|
6
12
|
require "net/http"
|
|
7
13
|
require "uri"
|
|
14
|
+
require_relative "ssrf"
|
|
8
15
|
|
|
9
16
|
module Tina4
|
|
10
17
|
class PushError < StandardError; end
|
|
@@ -34,7 +41,11 @@ module Tina4
|
|
|
34
41
|
bytes.b.rjust(32, "\x00".b)
|
|
35
42
|
end
|
|
36
43
|
|
|
37
|
-
def initialize(subject: nil, public_key: nil, private_key: nil, ttl: 60, urgency: nil)
|
|
44
|
+
def initialize(subject: nil, public_key: nil, private_key: nil, ttl: 60, urgency: nil, allow_hosts: nil)
|
|
45
|
+
# SSRF guard (ADR-0084): the push endpoint is refused when it resolves to a
|
|
46
|
+
# private/internal address unless TINA4_ALLOW_PRIVATE_REQUESTS is truthy or
|
|
47
|
+
# the host/CIDR is on this allow-list.
|
|
48
|
+
@allow_hosts = allow_hosts || []
|
|
38
49
|
@subject = (subject || ENV.fetch("TINA4_VAPID_SUBJECT", "")).strip
|
|
39
50
|
@public_key = (public_key || ENV.fetch("TINA4_VAPID_PUBLIC", "")).strip
|
|
40
51
|
@private_key = (private_key || ENV.fetch("TINA4_VAPID_PRIVATE", "")).strip
|
|
@@ -82,6 +93,13 @@ module Tina4
|
|
|
82
93
|
end
|
|
83
94
|
|
|
84
95
|
def deliver(endpoint, uri, subject, public_key, private, public, body)
|
|
96
|
+
# SSRF guard (ADR-0084): refuse a private/internal push endpoint. Net::HTTP
|
|
97
|
+
# does not follow redirects, so guarding the endpoint is sufficient.
|
|
98
|
+
begin
|
|
99
|
+
Tina4::Ssrf.guard_url!(uri, @allow_hosts)
|
|
100
|
+
rescue Tina4::SsrfError => e
|
|
101
|
+
raise PushError, e.message
|
|
102
|
+
end
|
|
85
103
|
request = Net::HTTP::Post.new(uri)
|
|
86
104
|
request["Authorization"] = "vapid t=#{vapid_token(uri, subject, private, public)}, k=#{public_key}"
|
|
87
105
|
request["Content-Encoding"] = "aes128gcm"
|
|
@@ -113,13 +131,13 @@ module Tina4
|
|
|
113
131
|
end
|
|
114
132
|
|
|
115
133
|
def self.b64(value)
|
|
116
|
-
Base64.urlsafe_encode64(value, padding: false)
|
|
134
|
+
Tina4::Base64.urlsafe_encode64(value, padding: false)
|
|
117
135
|
end
|
|
118
136
|
|
|
119
137
|
def decode(value, name)
|
|
120
138
|
raise PushError, "#{name} must be a non-empty base64url string" unless value.is_a?(String) && value.match?(/\A[A-Za-z0-9_-]+\z/)
|
|
121
139
|
|
|
122
|
-
Base64.urlsafe_decode64(value)
|
|
140
|
+
Tina4::Base64.urlsafe_decode64(value)
|
|
123
141
|
rescue ArgumentError => e
|
|
124
142
|
raise PushError, "#{name} must be base64url encoded: #{e.message}"
|
|
125
143
|
end
|
data/lib/tina4/query_builder.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
# QueryBuilder — Fluent SQL query builder.
|
data/lib/tina4/queue.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "securerandom"
|
|
4
10
|
require "uri"
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require_relative "../parse_json"
|
|
2
10
|
|
|
3
11
|
module Tina4
|
|
4
12
|
module QueueBackends
|
|
@@ -120,7 +128,7 @@ module Tina4
|
|
|
120
128
|
end
|
|
121
129
|
return nil unless msg
|
|
122
130
|
|
|
123
|
-
data =
|
|
131
|
+
data = Tina4.parse_json(msg.payload)
|
|
124
132
|
@last_message = msg
|
|
125
133
|
|
|
126
134
|
# attempts and error MUST be carried back. Rebuilding from
|
|
@@ -243,7 +251,7 @@ module Tina4
|
|
|
243
251
|
loop do
|
|
244
252
|
msg = reader.poll(500)
|
|
245
253
|
if msg
|
|
246
|
-
data =
|
|
254
|
+
data = Tina4.parse_json(msg.payload)
|
|
247
255
|
data["status"] = "dead"
|
|
248
256
|
out << data
|
|
249
257
|
next
|
|
@@ -259,6 +267,14 @@ module Tina4
|
|
|
259
267
|
out
|
|
260
268
|
end
|
|
261
269
|
|
|
270
|
+
# Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
|
|
271
|
+
def reject(job, reason = "")
|
|
272
|
+
job.attempts = [job.attempts + 1, @max_retries].max
|
|
273
|
+
job.error = reason
|
|
274
|
+
dead_letter(job)
|
|
275
|
+
acknowledge(job)
|
|
276
|
+
end
|
|
277
|
+
|
|
262
278
|
def failed(_topic, max_retries: 3)
|
|
263
279
|
raise NotImplementedError,
|
|
264
280
|
"The kafka queue backend cannot answer failed(): a job that failed " \
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "fileutils"
|
|
4
10
|
require "time"
|
|
@@ -171,6 +177,18 @@ module Tina4
|
|
|
171
177
|
# Aligns with retry_job(id) which had always unlinked the dead-letter
|
|
172
178
|
# file — two spellings of the same intent that previously diverged.
|
|
173
179
|
# Increments attempts, clears the error.
|
|
180
|
+
# Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
|
|
181
|
+
# Unlike fail(), which requeues until max_retries is spent, reject() moves
|
|
182
|
+
# a known-poison job straight to the dead-letter store. attempts is floored
|
|
183
|
+
# at max_retries so dead_letters (attempts >= limit) returns it and
|
|
184
|
+
# dead_letter_count agrees.
|
|
185
|
+
def reject(job, reason = "")
|
|
186
|
+
clear_reservation(job.topic, job.id)
|
|
187
|
+
job.attempts = [job.attempts + 1, @max_retries].max
|
|
188
|
+
job.error = reason
|
|
189
|
+
move_to_dead_letter(job, reason)
|
|
190
|
+
end
|
|
191
|
+
|
|
174
192
|
def retry(job, delay_seconds: 0)
|
|
175
193
|
clear_reservation(job.topic, job.id)
|
|
176
194
|
# Drop any dead-letter file for this id BEFORE the re-queue: if this
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module QueueBackends
|
|
@@ -278,6 +284,18 @@ module Tina4
|
|
|
278
284
|
# the normal path. This method did not exist at all, and Queue#failed
|
|
279
285
|
# silently returned [] for it — indistinguishable from "nothing has
|
|
280
286
|
# failed" (ADR-0022 decision 7).
|
|
287
|
+
# Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
|
|
288
|
+
def reject(job, reason = "")
|
|
289
|
+
job.attempts = [job.attempts + 1, @max_retries].max
|
|
290
|
+
job.error = reason
|
|
291
|
+
collection.find_one_and_update(
|
|
292
|
+
{ _id: job.id },
|
|
293
|
+
{ "$set" => { status: "dead", topic: "#{job.topic}.dead_letter",
|
|
294
|
+
attempts: job.attempts, error: reason, reserved_at: nil } },
|
|
295
|
+
upsert: true
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
281
299
|
def failed(topic, max_retries: 3)
|
|
282
300
|
collection.find(
|
|
283
301
|
topic: topic,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require_relative "../parse_json"
|
|
2
10
|
|
|
3
11
|
module Tina4
|
|
4
12
|
module QueueBackends
|
|
@@ -62,7 +70,7 @@ module Tina4
|
|
|
62
70
|
delivery_info, _properties, payload = queue.pop(manual_ack: true)
|
|
63
71
|
return nil unless payload
|
|
64
72
|
|
|
65
|
-
data =
|
|
73
|
+
data = Tina4.parse_json(payload)
|
|
66
74
|
# attempts and error MUST be carried back. Rebuilding the Job from
|
|
67
75
|
# topic/payload/id alone reset attempts to 0 on every redelivery, so
|
|
68
76
|
# fail()'s attempts >= max_retries check could never trip and a poison
|
|
@@ -169,6 +177,14 @@ module Tina4
|
|
|
169
177
|
end
|
|
170
178
|
end
|
|
171
179
|
|
|
180
|
+
# Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
|
|
181
|
+
def reject(job, reason = "")
|
|
182
|
+
job.attempts = [job.attempts + 1, @max_retries].max
|
|
183
|
+
job.error = reason
|
|
184
|
+
dead_letter(job)
|
|
185
|
+
complete(job)
|
|
186
|
+
end
|
|
187
|
+
|
|
172
188
|
def failed(_topic, max_retries: 3)
|
|
173
189
|
raise NotImplementedError,
|
|
174
190
|
"The rabbitmq queue backend cannot answer failed(): a job that " \
|
|
@@ -282,7 +298,7 @@ module Tina4
|
|
|
282
298
|
_info, _props, payload = queue.pop(manual_ack: false)
|
|
283
299
|
break unless payload
|
|
284
300
|
|
|
285
|
-
out <<
|
|
301
|
+
out << Tina4.parse_json(payload)
|
|
286
302
|
end
|
|
287
303
|
out.each { |data| publish_to(name, data) }
|
|
288
304
|
out
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "securerandom"
|
|
4
10
|
require "time"
|
|
5
11
|
require "uri"
|
|
6
12
|
require "cgi"
|
|
13
|
+
require_relative "parse_json"
|
|
7
14
|
|
|
8
15
|
module Tina4
|
|
9
16
|
# Middleware wrapper that tags requests arriving on the AI dev port.
|
|
@@ -503,6 +510,9 @@ module Tina4
|
|
|
503
510
|
|
|
504
511
|
# Skip underscore-prefixed segments — private files even within pages/.
|
|
505
512
|
return nil if clean_path.split("/").any? { |seg| seg.start_with?("_") }
|
|
513
|
+
# ADR-0082: a "." / ".." segment (or a backslash) could walk out of
|
|
514
|
+
# pages/ - the only directory that auto-routes. Refuse it outright.
|
|
515
|
+
return nil if clean_path.include?("\\") || clean_path.split("/", -1).any? { |seg| ["", ".", ".."].include?(seg) }
|
|
506
516
|
|
|
507
517
|
is_dev = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "false").downcase)
|
|
508
518
|
|
|
@@ -1179,68 +1189,46 @@ module Tina4
|
|
|
1179
1189
|
def self.enforce_route_auth(env, route)
|
|
1180
1190
|
return nil unless route.auth_required
|
|
1181
1191
|
|
|
1182
|
-
|
|
1183
|
-
token_source = nil # :header, :body, :session
|
|
1192
|
+
unauthorized = [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
1184
1193
|
|
|
1185
|
-
#
|
|
1194
|
+
# Token slots in priority order: Authorization Bearer header, then the
|
|
1195
|
+
# body formToken (frond.js puts the auth token it received as a
|
|
1196
|
+
# FreshToken there), then the session. The first slot holding a token
|
|
1197
|
+
# decides, EXCEPT that a form token ("type" => "form") proves where a
|
|
1198
|
+
# write came from, not who sent it: it is skipped and the next slot is
|
|
1199
|
+
# tried (ADR-0079 s1). So a logged-in user posting a rendered form (form
|
|
1200
|
+
# token in the body, auth token in the session) is still authenticated.
|
|
1201
|
+
candidates = []
|
|
1186
1202
|
auth_header = env["HTTP_AUTHORIZATION"] || ""
|
|
1187
|
-
if auth_header =~ /\ABearer\s+(.+)\z/i
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
end
|
|
1203
|
+
candidates << [Regexp.last_match(1), :header] if auth_header =~ /\ABearer\s+(.+)\z/i
|
|
1204
|
+
form_token = _extract_form_token(_read_rack_body(env), env)
|
|
1205
|
+
candidates << [form_token, :body] if form_token && !form_token.empty?
|
|
1191
1206
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
form_token = _extract_form_token(body_str, env)
|
|
1196
|
-
if form_token && !form_token.empty?
|
|
1197
|
-
token = form_token
|
|
1198
|
-
token_source = :body
|
|
1199
|
-
end
|
|
1207
|
+
verdict = candidates.each do |token, source|
|
|
1208
|
+
outcome = _authenticate_token(env, token, source)
|
|
1209
|
+
break outcome unless outcome == :skip
|
|
1200
1210
|
end
|
|
1211
|
+
return unauthorized if verdict == :denied
|
|
1201
1212
|
|
|
1202
|
-
|
|
1203
|
-
if token.nil?
|
|
1213
|
+
unless env.key?("tina4.auth_payload")
|
|
1204
1214
|
# Request path, so the same log-loud-then-degrade policy as
|
|
1205
1215
|
# Request#session (ADR-0021): an unreachable session store must not turn
|
|
1206
1216
|
# the auth gate into a 500. It degrades to an empty session, which means
|
|
1207
1217
|
# no token, which means the ordinary 401 below - a SERVED request.
|
|
1208
1218
|
session = Tina4::Session.new(env, degrade_on_backend_failure: true)
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1219
|
+
# A provider-verified OIDC identity counts only while it is live
|
|
1220
|
+
# (ADR-0079 s5).
|
|
1221
|
+
identity = Tina4::Sso.live_session_identity(session.get("_tina4_sso"))
|
|
1222
|
+
if identity
|
|
1212
1223
|
env["tina4.auth_payload"] = identity
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
session_token = session.get("token")
|
|
1219
|
-
if session_token && !session_token.empty?
|
|
1220
|
-
token = session_token
|
|
1221
|
-
token_source = :session
|
|
1222
|
-
end
|
|
1223
|
-
end
|
|
1224
|
-
|
|
1225
|
-
# API_KEY bypass — routed through the timing-safe Tina4::Auth.validate_api_key
|
|
1226
|
-
# (OpenSSL.fixed_length_secure_compare), matching tina4_python's _check_auth.
|
|
1227
|
-
# It used to be a plain `token == api_key`, which returns as soon as two
|
|
1228
|
-
# bytes differ — so response timing leaks the key prefix and the key can be
|
|
1229
|
-
# recovered a character at a time. validate_api_key also covers the unset /
|
|
1230
|
-
# blank / wrong-length cases the old guard spelled out by hand.
|
|
1231
|
-
if Tina4::Auth.validate_api_key(token)
|
|
1232
|
-
env["tina4.auth_payload"] = { "_auth" => "api_key" }
|
|
1233
|
-
elsif token
|
|
1234
|
-
unless Tina4::Auth.valid_token(token)
|
|
1235
|
-
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
1224
|
+
else
|
|
1225
|
+
session_token = session.get("token")
|
|
1226
|
+
if session_token.is_a?(String) && !session_token.empty?
|
|
1227
|
+
return unauthorized unless _authenticate_token(env, session_token, :session) == :accepted
|
|
1228
|
+
end
|
|
1236
1229
|
end
|
|
1237
|
-
env["tina4.auth_payload"] = Tina4::Auth.get_payload(token)
|
|
1238
|
-
|
|
1239
|
-
# When body formToken validates, store a refreshed token for the FreshToken response header
|
|
1240
|
-
env["tina4.fresh_token"] = Tina4::Auth.refresh_token(token) if token_source == :body
|
|
1241
|
-
else
|
|
1242
|
-
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
1243
1230
|
end
|
|
1231
|
+
return unauthorized unless env.key?("tina4.auth_payload")
|
|
1244
1232
|
|
|
1245
1233
|
# ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
1246
1234
|
# Auth has passed (401 ruled out above). If the route carries role/
|
|
@@ -1251,6 +1239,30 @@ module Tina4
|
|
|
1251
1239
|
nil
|
|
1252
1240
|
end
|
|
1253
1241
|
|
|
1242
|
+
# Judge one token from one slot: :accepted (env["tina4.auth_payload"] set),
|
|
1243
|
+
# :skip (a valid form token - not an identity, try the next slot) or
|
|
1244
|
+
# :denied (anything else).
|
|
1245
|
+
#
|
|
1246
|
+
# API_KEY bypass - routed through the timing-safe Tina4::Auth.validate_api_key
|
|
1247
|
+
# (OpenSSL.fixed_length_secure_compare), matching tina4_python's _check_auth.
|
|
1248
|
+
# A plain `token == api_key` returns as soon as two bytes differ, so response
|
|
1249
|
+
# timing would leak the key a character at a time.
|
|
1250
|
+
def self._authenticate_token(env, token, source)
|
|
1251
|
+
if Tina4::Auth.validate_api_key(token)
|
|
1252
|
+
env["tina4.auth_payload"] = { "_auth" => "api_key" }
|
|
1253
|
+
return :accepted
|
|
1254
|
+
end
|
|
1255
|
+
payload = Tina4::Auth.valid_token(token)
|
|
1256
|
+
return :denied unless payload
|
|
1257
|
+
return :skip unless Tina4::Auth.identity_payload?(payload)
|
|
1258
|
+
|
|
1259
|
+
env["tina4.auth_payload"] = payload
|
|
1260
|
+
# When a body token authenticates, store a refreshed token for the
|
|
1261
|
+
# FreshToken response header (identity tokens only).
|
|
1262
|
+
env["tina4.fresh_token"] = Tina4::Auth.refresh_token(token) if source == :body
|
|
1263
|
+
:accepted
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1254
1266
|
# Build a 403 Rack tuple when a route's RBAC guards are not satisfied by the
|
|
1255
1267
|
# verified payload, else nil. AND across guard groups, OR within a group.
|
|
1256
1268
|
# Roles read the `roles` claim (legacy singular `role` coerced); permissions
|
|
@@ -1316,7 +1328,7 @@ module Tina4
|
|
|
1316
1328
|
|
|
1317
1329
|
if content_type.include?("application/json")
|
|
1318
1330
|
begin
|
|
1319
|
-
parsed =
|
|
1331
|
+
parsed = Tina4.parse_json(body_str)
|
|
1320
1332
|
return parsed["formToken"] if parsed.is_a?(Hash) && parsed["formToken"]
|
|
1321
1333
|
rescue JSON::ParserError
|
|
1322
1334
|
# Not valid JSON — fall through
|
data/lib/tina4/rate_limiter.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
class RateLimiter
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "fileutils"
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -9,7 +15,15 @@ module Tina4
|
|
|
9
15
|
class S3Storage < StorageBackend
|
|
10
16
|
def initialize(endpoint: nil, key: nil, secret: nil, bucket: nil, region: nil)
|
|
11
17
|
super()
|
|
12
|
-
|
|
18
|
+
begin
|
|
19
|
+
require "aws-sdk-s3" # optional dependency, loaded lazily
|
|
20
|
+
rescue LoadError
|
|
21
|
+
# Storage.select rescues this and falls back to local; its warning
|
|
22
|
+
# carries this message, so the remedy reaches the operator.
|
|
23
|
+
raise LoadError,
|
|
24
|
+
"The 'aws-sdk-s3' gem is required for S3Storage. Install it with: " \
|
|
25
|
+
"bundle add aws-sdk-s3 (or add gem \"aws-sdk-s3\" to your Gemfile)"
|
|
26
|
+
end
|
|
13
27
|
|
|
14
28
|
@bucket = bucket || ENV["TINA4_STORAGE_BUCKET"]
|
|
15
29
|
raise ArgumentError, "S3Storage requires TINA4_STORAGE_BUCKET" if @bucket.nil? || @bucket.empty?
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "securerandom"
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
data/lib/tina4/realtime.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "time"
|
|
5
11
|
require "openssl"
|
|
6
|
-
|
|
12
|
+
require_relative "base64"
|
|
7
13
|
|
|
8
14
|
require_relative "realtime/workspace"
|
|
9
15
|
require_relative "realtime/channel"
|
|
@@ -14,6 +20,7 @@ require_relative "realtime/storage_backend"
|
|
|
14
20
|
require_relative "realtime/local_storage"
|
|
15
21
|
require_relative "realtime/s3_storage"
|
|
16
22
|
require_relative "realtime/storage"
|
|
23
|
+
require_relative "parse_json"
|
|
17
24
|
|
|
18
25
|
module Tina4
|
|
19
26
|
# Real-time collaboration mount for Tina4 (Ruby), parity with the Python
|
|
@@ -57,7 +64,7 @@ module Tina4
|
|
|
57
64
|
if turn_url && !turn_url.empty? && secret && !secret.empty?
|
|
58
65
|
ttl = (ENV["TINA4_RTC_TURN_TTL"] || "3600").to_i
|
|
59
66
|
username = (Time.now.to_i + ttl).to_s
|
|
60
|
-
credential = Base64.strict_encode64(OpenSSL::HMAC.digest("SHA1", secret, username))
|
|
67
|
+
credential = Tina4::Base64.strict_encode64(OpenSSL::HMAC.digest("SHA1", secret, username))
|
|
61
68
|
servers << { "urls" => split_urls(turn_url), "username" => username, "credential" => credential }
|
|
62
69
|
end
|
|
63
70
|
servers
|
|
@@ -312,7 +319,7 @@ module Tina4
|
|
|
312
319
|
def parse_json(data)
|
|
313
320
|
return data if data.is_a?(Hash)
|
|
314
321
|
|
|
315
|
-
|
|
322
|
+
Tina4.parse_json(data.to_s)
|
|
316
323
|
rescue JSON::ParserError, TypeError
|
|
317
324
|
nil
|
|
318
325
|
end
|