tina4ruby 3.13.137 → 3.13.138
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 +36 -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 +57 -7
- data/lib/tina4/auth.rb +18 -12
- 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 +6 -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 +6 -0
- 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 +6 -0
- data/lib/tina4/cli.rb +22 -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 +215 -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 +8 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +6 -0
- 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 +196 -56
- 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 +28 -27
- 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 +6 -0
- 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 +15 -8
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +24 -9
- 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 +9 -3
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +10 -2
- data/lib/tina4/queue_backends/lite_backend.rb +6 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +6 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +10 -2
- data/lib/tina4/rack_app.rb +11 -1
- 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 +6 -0
- 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 +85 -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 +6 -0
- 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 +25 -8
- 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 +10 -4
- data/lib/tina4/websocket_backplane.rb +19 -2
- data/lib/tina4/wsdl.rb +31 -16
- data/lib/tina4/xml_parser.rb +259 -0
- data/lib/tina4.rb +87 -31
- data/lib/tina4ruby.rb +6 -0
- metadata +43 -140
data/lib/tina4/auth.rb
CHANGED
|
@@ -1,9 +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
|
require "openssl"
|
|
3
|
-
|
|
9
|
+
require_relative "base64"
|
|
4
10
|
require "json"
|
|
5
11
|
require "fileutils"
|
|
6
12
|
require "securerandom"
|
|
13
|
+
require_relative "parse_json"
|
|
14
|
+
require_relative "secret_file"
|
|
7
15
|
|
|
8
16
|
module Tina4
|
|
9
17
|
module Auth
|
|
@@ -100,12 +108,10 @@ module Tina4
|
|
|
100
108
|
local_path = File.join(root_dir, ".env.local")
|
|
101
109
|
# If the file exists and does not end in a newline, prepend one so the
|
|
102
110
|
# new key lands on its own line rather than gluing onto the last value.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
content
|
|
106
|
-
prefix = "\n" if !content.empty? && !content.end_with?("\n")
|
|
111
|
+
SecretFile.update(local_path) do |content|
|
|
112
|
+
prefix = !content.empty? && !content.end_with?("\n") ? "\n" : ""
|
|
113
|
+
content + "#{prefix}TINA4_SECRET=#{new_secret}\n"
|
|
107
114
|
end
|
|
108
|
-
File.open(local_path, "a") { |f| f.write("#{prefix}TINA4_SECRET=#{new_secret}\n") }
|
|
109
115
|
log_info("Auth: generated a development secret, saved to .env.local (gitignored)")
|
|
110
116
|
rescue StandardError => e
|
|
111
117
|
# Keep the in-memory secret for this run; just warn. Never crash boot.
|
|
@@ -139,7 +145,7 @@ module Tina4
|
|
|
139
145
|
|
|
140
146
|
# Base64url-encode without padding (JWT spec)
|
|
141
147
|
def base64url_encode(data)
|
|
142
|
-
Base64.urlsafe_encode64(data, padding: false)
|
|
148
|
+
Tina4::Base64.urlsafe_encode64(data, padding: false)
|
|
143
149
|
end
|
|
144
150
|
|
|
145
151
|
# Base64url-decode (handles missing padding)
|
|
@@ -147,7 +153,7 @@ module Tina4
|
|
|
147
153
|
# Add back padding
|
|
148
154
|
remainder = str.length % 4
|
|
149
155
|
str += "=" * ((4 - remainder) % 4) if remainder != 0
|
|
150
|
-
Base64.urlsafe_decode64(str)
|
|
156
|
+
Tina4::Base64.urlsafe_decode64(str)
|
|
151
157
|
end
|
|
152
158
|
|
|
153
159
|
# Pick the JWT algorithm: explicit argument, else TINA4_JWT_ALGORITHM, else
|
|
@@ -200,10 +206,10 @@ module Tina4
|
|
|
200
206
|
def decode_envelope(token, algorithm)
|
|
201
207
|
parts = token.to_s.split(".")
|
|
202
208
|
return nil unless parts.length == 3
|
|
203
|
-
return nil unless
|
|
209
|
+
return nil unless Tina4.parse_json(base64url_decode(parts[0]))["alg"] == algorithm
|
|
204
210
|
return nil unless yield("#{parts[0]}.#{parts[1]}", base64url_decode(parts[2]))
|
|
205
211
|
|
|
206
|
-
payload =
|
|
212
|
+
payload = Tina4.parse_json(base64url_decode(parts[1]))
|
|
207
213
|
now = Time.now.to_i
|
|
208
214
|
|
|
209
215
|
# RFC 7519 s4.1.4: "The processing of the 'exp' claim requires that the
|
|
@@ -361,7 +367,7 @@ module Tina4
|
|
|
361
367
|
return nil unless parts.length == 3
|
|
362
368
|
|
|
363
369
|
payload_json = base64url_decode(parts[1])
|
|
364
|
-
|
|
370
|
+
Tina4.parse_json(payload_json)
|
|
365
371
|
rescue ArgumentError, JSON::ParserError
|
|
366
372
|
nil
|
|
367
373
|
end
|
|
@@ -564,7 +570,7 @@ module Tina4
|
|
|
564
570
|
def generate_keys
|
|
565
571
|
Tina4::Log.info("Generating RSA key pair for JWT authentication")
|
|
566
572
|
key = OpenSSL::PKey::RSA.generate(2048)
|
|
567
|
-
|
|
573
|
+
SecretFile.update(private_key_path) { key.to_pem }
|
|
568
574
|
File.write(public_key_path, key.public_key.to_pem)
|
|
569
575
|
@private_key = nil
|
|
570
576
|
@public_key = nil
|
data/lib/tina4/auto_crud.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
|
|
|
4
10
|
module Tina4
|
|
@@ -105,17 +111,46 @@ module Tina4
|
|
|
105
111
|
page = [page, 1].max
|
|
106
112
|
limit = per_page
|
|
107
113
|
offset = req.query["offset"] ? req.query["offset"].to_i : (page - 1) * per_page
|
|
108
|
-
order_by = parse_sort(req.query["sort"])
|
|
109
114
|
|
|
110
|
-
# Filter support: ?filter[field]=value
|
|
115
|
+
# Filter support: ?filter[field]=value. ADR-0069: ANY bracket key is
|
|
116
|
+
# captured and must resolve to a declared field (attribute or its
|
|
117
|
+
# mapped column); an unknown key is a 400 before any SQL runs.
|
|
118
|
+
#
|
|
119
|
+
# The query parser keeps bracketed keys flat, so a list or map shows
|
|
120
|
+
# up as "sort[...]" or "filter[name][...]": a 400, never ignored.
|
|
121
|
+
if req.query.keys.any? { |key| key.start_with?("sort[") }
|
|
122
|
+
next res.error("INVALID_QUERY_PARAMETER",
|
|
123
|
+
"Query parameter 'sort' must be a single comma-separated string", 400)
|
|
124
|
+
end
|
|
125
|
+
|
|
111
126
|
filter_conditions = []
|
|
112
127
|
filter_values = []
|
|
128
|
+
unknown_filter_field = nil
|
|
129
|
+
nested_filter_field = nil
|
|
113
130
|
req.query.each do |key, value|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
next unless key =~ /\Afilter\[(.*)\]\z/m
|
|
132
|
+
|
|
133
|
+
filter_key = Regexp.last_match(1)
|
|
134
|
+
if filter_key.include?("][")
|
|
135
|
+
nested_filter_field = filter_key.split("][", 2).first
|
|
136
|
+
break
|
|
117
137
|
end
|
|
138
|
+
column = model_class.resolve_field_column(filter_key)
|
|
139
|
+
unless column
|
|
140
|
+
unknown_filter_field = filter_key
|
|
141
|
+
break
|
|
142
|
+
end
|
|
143
|
+
filter_conditions << "#{column} = ?"
|
|
144
|
+
filter_values << value
|
|
145
|
+
end
|
|
146
|
+
if nested_filter_field
|
|
147
|
+
next res.error("INVALID_QUERY_PARAMETER",
|
|
148
|
+
"Filter value for '#{nested_filter_field}' must be a single value", 400)
|
|
118
149
|
end
|
|
150
|
+
next res.error("UNKNOWN_FIELD", "Unknown filter field '#{unknown_filter_field}'", 400) if unknown_filter_field
|
|
151
|
+
|
|
152
|
+
order_by, unknown_sort_field = parse_sort(model_class, req.query["sort"])
|
|
153
|
+
next res.error("UNKNOWN_FIELD", "Unknown sort field '#{unknown_sort_field}'", 400) if unknown_sort_field
|
|
119
154
|
|
|
120
155
|
if filter_conditions.empty?
|
|
121
156
|
records = model_class.all(limit: limit, offset: offset, order_by: order_by)
|
|
@@ -150,8 +185,7 @@ module Tina4
|
|
|
150
185
|
# GET /api/{table}/{id} -- get single record
|
|
151
186
|
Tina4::Router.add("GET", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
152
187
|
begin
|
|
153
|
-
|
|
154
|
-
record = model_class.find_by_id(id.to_i)
|
|
188
|
+
record = find_addressed_record(model_class, req.params["id"])
|
|
155
189
|
if record
|
|
156
190
|
res.json({ data: record.to_h })
|
|
157
191
|
else
|
|
@@ -200,8 +234,7 @@ module Tina4
|
|
|
200
234
|
# PUT /api/{table}/{id} -- update record
|
|
201
235
|
put_route = Tina4::Router.add("PUT", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
202
236
|
begin
|
|
203
|
-
|
|
204
|
-
record = model_class.find_by_id(id.to_i)
|
|
237
|
+
record = find_addressed_record(model_class, req.params["id"])
|
|
205
238
|
unless record
|
|
206
239
|
next res.json({ error: "Not found" }, status: 404)
|
|
207
240
|
end
|
|
@@ -236,8 +269,7 @@ module Tina4
|
|
|
236
269
|
# DELETE /api/{table}/{id} -- delete record
|
|
237
270
|
delete_route = Tina4::Router.add("DELETE", "#{prefix}/#{table}/{id}", proc { |req, res|
|
|
238
271
|
begin
|
|
239
|
-
|
|
240
|
-
record = model_class.find_by_id(id.to_i)
|
|
272
|
+
record = find_addressed_record(model_class, req.params["id"])
|
|
241
273
|
unless record
|
|
242
274
|
next res.json({ error: "Not found" }, status: 404)
|
|
243
275
|
end
|
|
@@ -314,27 +346,51 @@ module Tina4
|
|
|
314
346
|
auto_increment = single_pk && defs[single_pk.to_sym] && defs[single_pk.to_sym][:auto_increment]
|
|
315
347
|
strip_pk = is_create ? !(single_pk && !auto_increment) : true
|
|
316
348
|
|
|
349
|
+
# ADR-0069: a body key is written only when it resolves to a DECLARED
|
|
350
|
+
# field, by attribute name or its mapped column - the SAME resolver the
|
|
351
|
+
# list route's filter/sort use. Anything else is dropped (writes drop,
|
|
352
|
+
# reads 400).
|
|
317
353
|
data.each_with_object({}) do |(key, value), allowed|
|
|
318
|
-
|
|
319
|
-
next unless
|
|
320
|
-
next if key_s == "is_deleted"
|
|
321
|
-
next if strip_pk && pk_fields.include?(key_s)
|
|
354
|
+
attribute = model_class.resolve_field(key)
|
|
355
|
+
next unless attribute
|
|
322
356
|
|
|
323
|
-
|
|
357
|
+
attribute_name = attribute.to_s
|
|
358
|
+
next if attribute_name == "is_deleted"
|
|
359
|
+
next if strip_pk && pk_fields.include?(attribute_name)
|
|
360
|
+
|
|
361
|
+
allowed[attribute_name] = value
|
|
324
362
|
end
|
|
325
363
|
end
|
|
326
364
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
365
|
+
# The row a URL {id} addresses, or nil. The id is bound, never
|
|
366
|
+
# interpolated; ORM.coerce_primary_key keeps a string natural key as-is
|
|
367
|
+
# and answers nil for a non-integer id on an integer key (-> 404).
|
|
368
|
+
def find_addressed_record(model_class, raw_id)
|
|
369
|
+
id = model_class.coerce_primary_key(raw_id)
|
|
370
|
+
id.nil? ? nil : model_class.find_by_id(id)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Parse the sort parameter: "-name,created_at" => "name DESC, created_at ASC".
|
|
374
|
+
#
|
|
375
|
+
# ADR-0069: each part (empty parts skipped, a leading "-" means DESC) must
|
|
376
|
+
# resolve to a declared field; the ORDER BY is built ONLY from resolved
|
|
377
|
+
# columns plus ASC/DESC. Returns [order_by_or_nil, unknown_field_or_nil].
|
|
378
|
+
def parse_sort(model_class, sort_str)
|
|
379
|
+
return [nil, nil] if sort_str.nil? || sort_str.empty?
|
|
380
|
+
|
|
381
|
+
parts = []
|
|
382
|
+
sort_str.split(",").each do |part|
|
|
383
|
+
part = part.strip
|
|
384
|
+
next if part.empty?
|
|
385
|
+
|
|
386
|
+
descending = part.start_with?("-")
|
|
387
|
+
field = descending ? part[1..].strip : part
|
|
388
|
+
column = model_class.resolve_field_column(field)
|
|
389
|
+
return [nil, field] unless column
|
|
390
|
+
|
|
391
|
+
parts << "#{column} #{descending ? 'DESC' : 'ASC'}"
|
|
392
|
+
end
|
|
393
|
+
[parts.empty? ? nil : parts.join(", "), nil]
|
|
338
394
|
end
|
|
339
395
|
end
|
|
340
396
|
end
|
data/lib/tina4/background.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
|
# Periodic background task registry.
|
|
@@ -9,7 +15,7 @@ module Tina4
|
|
|
9
15
|
#
|
|
10
16
|
# Ruby has no asyncio event loop, so each task runs in its own dedicated OS
|
|
11
17
|
# thread, started at registration time. Because the thread runs regardless of
|
|
12
|
-
# which web server (Puma
|
|
18
|
+
# which web server (the built-in server or Puma) is in front, a Ruby background task is never
|
|
13
19
|
# a silent no-op under production — the thread IS the runtime (contrast the
|
|
14
20
|
# Python ASGI / PHP-FPM silent-no-op the other frameworks had to fix). The GIL
|
|
15
21
|
# keeps it cooperative-enough for the periodic work this is meant for (queue
|
data/lib/tina4/base64.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
module Tina4
|
|
10
|
+
# Base64 on core Array#pack / String#unpack1, so Tina4 needs no base64 gem.
|
|
11
|
+
#
|
|
12
|
+
# The base64 gem stopped being a default gem in Ruby 3.4, and a bundled gem an
|
|
13
|
+
# app does not declare is not on the load path under Bundler. The gem is itself
|
|
14
|
+
# a thin wrapper over pack("m"), so this module keeps its method names and its
|
|
15
|
+
# exact semantics (same bytes out, same ArgumentError on bad strict input) and
|
|
16
|
+
# the call sites read the same with a Tina4:: prefix.
|
|
17
|
+
module Base64
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# RFC 2045 (MIME): a newline after every 60 encoded characters and at the end.
|
|
21
|
+
def encode64(binary)
|
|
22
|
+
[binary].pack("m")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Lenient decode: characters outside the alphabet are skipped, never raised on.
|
|
26
|
+
def decode64(string)
|
|
27
|
+
string.unpack1("m")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# RFC 4648: no line feeds.
|
|
31
|
+
def strict_encode64(binary)
|
|
32
|
+
[binary].pack("m0")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raises ArgumentError on anything that is not canonical, padded base64.
|
|
36
|
+
def strict_decode64(string)
|
|
37
|
+
string.unpack1("m0")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# RFC 4648 URL-safe alphabet ("-" and "_"), padding optional.
|
|
41
|
+
def urlsafe_encode64(binary, padding: true)
|
|
42
|
+
encoded = strict_encode64(binary)
|
|
43
|
+
encoded.chomp!("==") or encoded.chomp!("=") unless padding
|
|
44
|
+
encoded.tr!("+/", "-_")
|
|
45
|
+
encoded
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Accepts padded or unpadded input; raises ArgumentError on invalid input.
|
|
49
|
+
def urlsafe_decode64(string)
|
|
50
|
+
if !string.end_with?("=") && string.length % 4 != 0
|
|
51
|
+
string = string.ljust((string.length + 3) & ~3, "=")
|
|
52
|
+
string.tr!("-_", "+/")
|
|
53
|
+
else
|
|
54
|
+
string = string.tr("-_", "+/")
|
|
55
|
+
end
|
|
56
|
+
strict_decode64(string)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/tina4/cache.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
module Tina4
|
|
2
8
|
# In-memory TTL cache with tag-based invalidation.
|
|
3
9
|
#
|
|
@@ -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 CacheBackends
|
|
@@ -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 "json"
|
|
4
10
|
require_relative "base_backend"
|
|
@@ -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 "json"
|
|
4
10
|
require "fileutils"
|
|
@@ -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 "json"
|
|
4
10
|
require "socket"
|
|
@@ -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_relative "base_backend"
|
|
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
|
require "json"
|
|
4
10
|
require "uri"
|
|
@@ -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 "json"
|
|
4
10
|
require "socket"
|
|
@@ -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_relative "redis_backend"
|
|
4
10
|
|
data/lib/tina4/cache_backends.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
|
require_relative "cache_backends/base_backend"
|
|
4
10
|
require_relative "cache_backends/memory_backend"
|
data/lib/tina4/cli.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
|
require "optparse"
|
|
4
10
|
require "fileutils"
|
|
@@ -8,6 +14,7 @@ require "tmpdir" # Dir.mktmpdir — used by the ADR-0063 sandbox edit-hint sc
|
|
|
8
14
|
require "stringio" # StringIO — captures generator stdout during the sandbox run
|
|
9
15
|
require "open3" # Open3.capture2e — captures `ruby -c` output+status for `lint`
|
|
10
16
|
require_relative "port_takeover"
|
|
17
|
+
require_relative "env" # Tina4::Env.is_truthy -- resolve_config runs before lib/tina4 loads
|
|
11
18
|
|
|
12
19
|
module Tina4
|
|
13
20
|
class CLI
|
|
@@ -676,7 +683,7 @@ module Tina4
|
|
|
676
683
|
# invokes PHP. That mismatch is why Ruby could not be launched through
|
|
677
684
|
# the shared launcher the other frameworks use.
|
|
678
685
|
opts.on("--managed", "Running under the tina4 CLI supervisor") { options[:managed] = true }
|
|
679
|
-
opts.on("--production", "
|
|
686
|
+
opts.on("--production", "Production mode: Puma if the app bundles it, else the built-in server") { options[:production] = true }
|
|
680
687
|
opts.on("--no-browser", "Do not open browser on start") { options[:no_browser] = true }
|
|
681
688
|
opts.on("--no-reload", "Disable file watcher / live-reload") { options[:no_reload] = true }
|
|
682
689
|
opts.on("--no-kill", "Never take over the port from a stale dev server") { options[:no_kill] = true }
|
|
@@ -727,9 +734,9 @@ module Tina4
|
|
|
727
734
|
|
|
728
735
|
is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
|
|
729
736
|
|
|
730
|
-
#
|
|
731
|
-
#
|
|
732
|
-
# Same production server, same shutdown contract as Tina4.run! - one
|
|
737
|
+
# Puma only with --production AND only when the app bundles it
|
|
738
|
+
# (ADR-0067); otherwise Tina4's built-in server, in development and
|
|
739
|
+
# production alike. Same production server, same shutdown contract as Tina4.run! - one
|
|
733
740
|
# implementation, so the two entry points cannot drift again (the old
|
|
734
741
|
# copy here claimed "we handle DB cleanup" and did no cleanup at all).
|
|
735
742
|
# TINA4_DEFAULT_WEBSERVER=TRUE pins the built-in server even with
|
|
@@ -739,7 +746,6 @@ module Tina4
|
|
|
739
746
|
return
|
|
740
747
|
end
|
|
741
748
|
|
|
742
|
-
Tina4::Log.info("Development server: WEBrick")
|
|
743
749
|
server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
|
|
744
750
|
server.start
|
|
745
751
|
end
|
|
@@ -3806,7 +3812,7 @@ module Tina4
|
|
|
3806
3812
|
# loopback, not 0.0.0.0. Only the DEFAULT changes: production
|
|
3807
3813
|
# (TINA4_DEBUG off) keeps 0.0.0.0, and a developer who WANTS network
|
|
3808
3814
|
# exposure sets TINA4_HOST=0.0.0.0 to override deliberately.
|
|
3809
|
-
Tina4.
|
|
3815
|
+
Tina4::Env.is_truthy(ENV["TINA4_DEBUG"]) ? "127.0.0.1" : DEFAULT_HOST
|
|
3810
3816
|
end
|
|
3811
3817
|
end
|
|
3812
3818
|
|
|
@@ -3875,9 +3881,18 @@ module Tina4
|
|
|
3875
3881
|
|
|
3876
3882
|
# Gemfile
|
|
3877
3883
|
unless File.exist?(File.join(dir, "Gemfile"))
|
|
3884
|
+
# Same Gemfile `tina4 init ruby` writes. The gem is published as
|
|
3885
|
+
# "tina4ruby" ("tina4-ruby" does not exist, so bundle install failed),
|
|
3886
|
+
# and sqlite3 is an app dependency (ADR-0067): tina4ruby loads it lazily
|
|
3887
|
+
# for the default SQLite database but never declares it.
|
|
3878
3888
|
File.write(File.join(dir, "Gemfile"), <<~RUBY)
|
|
3879
3889
|
source "https://rubygems.org"
|
|
3880
|
-
|
|
3890
|
+
|
|
3891
|
+
gem "tina4ruby", "~> 3.0"
|
|
3892
|
+
|
|
3893
|
+
# SQLite driver for the default database (TINA4_DATABASE_URL=sqlite:...).
|
|
3894
|
+
# tina4ruby loads it lazily; it is an app dependency, like pg or mysql2.
|
|
3895
|
+
gem "sqlite3"
|
|
3881
3896
|
RUBY
|
|
3882
3897
|
end
|
|
3883
3898
|
|
data/lib/tina4/constants.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Tina4 Constants — HTTP status codes and content types.
|
|
2
8
|
#
|
|
3
9
|
# Standard constants for use in route handlers across all Tina4 frameworks.
|
|
@@ -59,25 +65,23 @@ module Tina4
|
|
|
59
65
|
304 => "Not Modified", 307 => "Temporary Redirect", 308 => "Permanent Redirect",
|
|
60
66
|
400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden",
|
|
61
67
|
404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable",
|
|
68
|
+
408 => "Request Timeout",
|
|
62
69
|
409 => "Conflict", 410 => "Gone", 413 => "Content Too Large",
|
|
63
70
|
415 => "Unsupported Media Type", 422 => "Unprocessable Content",
|
|
64
|
-
429 => "Too Many Requests",
|
|
71
|
+
429 => "Too Many Requests", 431 => "Request Header Fields Too Large",
|
|
65
72
|
500 => "Internal Server Error", 501 => "Not Implemented",
|
|
66
|
-
502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout"
|
|
73
|
+
502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout",
|
|
74
|
+
505 => "HTTP Version Not Supported"
|
|
67
75
|
}.freeze
|
|
68
76
|
|
|
69
77
|
# Return the canonical HTTP reason phrase for ``status``.
|
|
70
78
|
#
|
|
71
79
|
# Falls back to a sensible label when an exotic status is used. Never
|
|
72
80
|
# returns an empty string — the HTTP/1.1 status line requires a phrase.
|
|
73
|
-
#
|
|
74
|
-
#
|
|
81
|
+
# The table is Tina4's own (the phrases match Python's and PHP's); it no
|
|
82
|
+
# longer defers to Rack::Utils, which is not a dependency.
|
|
75
83
|
def self.http_reason(status)
|
|
76
84
|
code = status.to_i
|
|
77
|
-
if defined?(Rack::Utils::HTTP_STATUS_CODES)
|
|
78
|
-
phrase = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
79
|
-
return phrase if phrase && !phrase.empty?
|
|
80
|
-
end
|
|
81
85
|
phrase = HTTP_REASON_PHRASES[code]
|
|
82
86
|
return phrase if phrase && !phrase.empty?
|
|
83
87
|
return "OK" if code >= 200 && code < 300
|
data/lib/tina4/container.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
|
# Lightweight dependency injection container.
|
|
@@ -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
|
# Tina4 Ruby
|
|
4
10
|
#
|
data/lib/tina4/context.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
|
# Tina4 Ruby
|
|
4
10
|
#
|
|
@@ -6,9 +12,10 @@
|
|
|
6
12
|
#
|
|
7
13
|
# Lets a Tina4 app ground its own AI assistant on its own source, offline: it
|
|
8
14
|
# walks the project, chunks code on def/class/module boundaries and docs as
|
|
9
|
-
# prose, and answers keyword/fuzzy queries over a SQLite FTS5 index.
|
|
10
|
-
# sqlite3 gem
|
|
11
|
-
# FTS5 + bm25() built in, so
|
|
15
|
+
# prose, and answers keyword/fuzzy queries over a SQLite FTS5 index. It uses the
|
|
16
|
+
# sqlite3 gem the APP declares (the `tina4 init ruby` Gemfile carries it; tina4ruby
|
|
17
|
+
# itself does not, ADR-0067) and modern SQLite ships FTS5 + bm25() built in, so
|
|
18
|
+
# this adds NO dependency. Without the gem the index reports itself unavailable.
|
|
12
19
|
#
|
|
13
20
|
# The retrieval core is a thin port of tina4-python's tina4_python/context
|
|
14
21
|
# (itself the proven slice of neemee: SqliteFTS + the stable
|
|
@@ -29,6 +36,7 @@ require "pathname"
|
|
|
29
36
|
require "fileutils"
|
|
30
37
|
require "set"
|
|
31
38
|
require_relative "context/chunker"
|
|
39
|
+
require_relative "sqlite3_gem"
|
|
32
40
|
|
|
33
41
|
module Tina4
|
|
34
42
|
class Context
|
|
@@ -70,7 +78,7 @@ module Tina4
|
|
|
70
78
|
@available = !!check.call
|
|
71
79
|
return unless @available
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
Tina4.require_sqlite3!
|
|
74
82
|
parent = File.dirname(@path)
|
|
75
83
|
FileUtils.mkdir_p(parent) unless parent.empty? || parent == "." || File.directory?(parent)
|
|
76
84
|
# One connection guarded by a Mutex — the dev-MCP reload hook may run on a
|
|
@@ -82,7 +90,7 @@ module Tina4
|
|
|
82
90
|
# ── FTS5 availability ───────────────────────────────────────
|
|
83
91
|
# Whether this Ruby's sqlite3 build supports FTS5 (a real in-memory probe).
|
|
84
92
|
def self.fts5_available?
|
|
85
|
-
|
|
93
|
+
Tina4.require_sqlite3!
|
|
86
94
|
db = SQLite3::Database.new(":memory:")
|
|
87
95
|
begin
|
|
88
96
|
db.execute("CREATE VIRTUAL TABLE _probe USING fts5(x)")
|