tina4ruby 3.13.139 → 3.13.140

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: 49ef08b44463d47d804c054dce8bb194276945134a618369e9d951ce239fd4cb
4
- data.tar.gz: 69b761361564c42e91584a7029fcd5fb067d2d3dc9103614f37662a7a17be707
3
+ metadata.gz: 68bda3e81c456588a9a4db6501cb402b1eaa570d5f2bfbee0efb83bd9cb61beb
4
+ data.tar.gz: 61271a97c34bf49f68fcaf605d955d6e4f932f72fa370c884f4f0e91ef432e91
5
5
  SHA512:
6
- metadata.gz: c7db9c5c8d7c0242fdbb2e3a7b43b80b5567d46443a3b029e7e546daf4b8f6eb508b7671ae077e3a661e8f07bf15e0ba809fe9e5259e5bb7117ee67afc35828d
7
- data.tar.gz: 975471f353c0f9804bc7678e9f2bafdd8f47d7eb44f50d54347ea08454b8560fc15b3bb8c740851cef08926a28f349e7a8121c5c09e7ca90f4e9a797f418ad0d
6
+ metadata.gz: 847b4f0439265406da7152292469d91b081a04e468f177b78980c9b669c96e60cca17ac06820bbdb5c66034802145ff64b294c7949136e2392236575170aeb3e
7
+ data.tar.gz: f690d2726ce41de740e44b6db7cc0cd3104ac40cace6c30dc5a4a94eef832f7e09128608b9cfaebf6e167687403917ca14c156d5f4b94bd65b8c46930d191bce
data/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ 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.140 — 2026-09-28
10
+
11
+ A housekeeping release that tightens the guards and dedups the SQLite plumbing. The CLAUDE.md doc-drift gate now watches this repo, so a doc that names a Tina4 method the code no longer has fails loudly instead of drifting on quietly. The SQLite path resolver learnt some manners: an escaping relative path is refused, and it creates the parent folders itself at 0775 (ADR-0086). The framework still carries no required runtime dependencies.
12
+
13
+ ### Fixes
14
+ - SQLite refuses a relative path that escapes the project and creates its parent directories at 0775, hardening ADR-0086.
15
+ - The maintainer skills stale check resolves correctly and warns when an addGlobal closure would go stale (skill bugs 90/91).
16
+
17
+ ### Refactors
18
+ - The SQLite `resolve_path` logic moves into one shared `SqlitePathResolver` module, so every caller reads the same path the same way.
19
+ - `symbolize_rows` and `symbolize_keys` collapse into one shared module, characterised first so the behaviour cannot shift.
20
+
21
+ ### Docs
22
+ - The Web Push skill mirrors sync back to the `.claude` canonical, so the two copies agree.
23
+
24
+ ### CI
25
+ - A CLAUDE.md doc-drift gate lands, the tina4-js rtc.md mirror syncs, and the SQLTranslator doc drift is fixed.
26
+ - The CLI crud generator locks in single pluralisation, so an `Order` model becomes `orders`, never `orderss`.
27
+
9
28
  ## 3.13.139
10
29
 
11
30
  Outbound requests from the Api client and Web Push are guarded against SSRF: private, loopback and link-local targets are refused unless explicitly allowed (ADR-0084). The development admin surface is hardened -- the health endpoint no longer discloses the framework version outside debug, and development binds loopback by default (ADR-0078). Production refuses to boot with a blank or short HMAC secret so tokens cannot be forged (ADR-0079). Queue reject() dead-letters immediately and size("dead") reports the dead-letter depth (ADR-0022, ADR-0023). Session table first-use is concurrency-safe across every engine. Mail encryption follows the configured TLS/STARTTLS contract (ADR-0071). Log output, driver-install hints and WSDL/mail diagnostics redact credentials and URLs. Publishing is gated behind a protected environment with release actions pinned by commit SHA, and a medium-severity API token-leak path is closed. The framework still has no required runtime dependencies.
@@ -7,10 +7,16 @@
7
7
 
8
8
 
9
9
  require_relative "../sqlite3_gem"
10
+ require_relative "../sqlite_path_resolver"
11
+ require_relative "../row_symbolizer"
10
12
 
11
13
  module Tina4
12
14
  module Adapters
13
15
  class Sqlite3Adapter
16
+ # Shared SQLite path resolution — one home for both SQLite resolvers.
17
+ extend Tina4::SqlitePathResolver
18
+ include Tina4::RowSymbolizer
19
+
14
20
  attr_reader :connection, :db_path
15
21
 
16
22
  def initialize(connection_string = nil)
@@ -30,32 +36,6 @@ module Tina4
30
36
  self
31
37
  end
32
38
 
33
- # Resolve a SQLite URL / path against the project root (cwd).
34
- # Convention matches Tina4::Drivers::SqliteDriver.resolve_path —
35
- # three slashes = relative, four = absolute, drive letter = Windows abs.
36
- def self.resolve_path(connection_string)
37
- return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
38
-
39
- raw = connection_string.to_s
40
- .sub(/^sqlite3?:\/\/\//, "")
41
- .sub(/^sqlite3?:\/\//, "")
42
- .sub(/^sqlite3?:/, "")
43
- return ":memory:" if raw == ":memory:"
44
-
45
- is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
46
- is_unix_abs = raw.start_with?("/")
47
-
48
- if is_windows_abs || is_unix_abs
49
- raw
50
- else
51
- resolved = File.join(Dir.pwd, raw)
52
- parent = File.dirname(resolved)
53
- require "fileutils"
54
- FileUtils.mkdir_p(parent) unless File.directory?(parent)
55
- resolved
56
- end
57
- end
58
-
59
39
  def close
60
40
  @connection.close if @connection
61
41
  @connection = nil
@@ -163,34 +143,6 @@ module Tina4
163
143
  def apply_limit(sql, limit, offset = 0)
164
144
  "#{sql}\nLIMIT #{limit} OFFSET #{offset}"
165
145
  end
166
-
167
- private
168
-
169
- # Symbolize a whole result set's keys, computing the mapping ONCE per query
170
- # rather than per cell. See the matching helper in drivers/sqlite_driver.rb
171
- # for the rationale: the per-row form ran k.to_s.to_sym for every cell, so a
172
- # 5,000-row x 6-column fetch did 30,000 conversions for 6 distinct keys. The
173
- # is_a? guard still drops any positional Integer keys older gems emit.
174
- def symbolize_rows(rows)
175
- return rows if rows.empty?
176
-
177
- str_keys = rows.first.keys.select { |k| k.is_a?(String) || k.is_a?(Symbol) }
178
- sym_keys = str_keys.map(&:to_sym)
179
- count = str_keys.length
180
- rows.map do |row|
181
- out = {}
182
- i = 0
183
- while i < count
184
- out[sym_keys[i]] = row[str_keys[i]]
185
- i += 1
186
- end
187
- out
188
- end
189
- end
190
-
191
- def symbolize_keys(hash)
192
- symbolize_rows([hash]).first
193
- end
194
146
  end
195
147
  end
196
148
  end
@@ -8,11 +8,17 @@
8
8
 
9
9
  require_relative "schema_split"
10
10
  require_relative "../sqlite3_gem"
11
+ require_relative "../sqlite_path_resolver"
12
+ require_relative "../row_symbolizer"
11
13
 
12
14
  module Tina4
13
15
  module Drivers
14
16
  class SqliteDriver
17
+ # Shared SQLite path resolution — one home for both SQLite resolvers.
18
+ extend Tina4::SqlitePathResolver
19
+
15
20
  include Tina4::DatabaseAdapter
21
+ include Tina4::RowSymbolizer
16
22
  include SchemaSplit
17
23
  attr_reader :connection
18
24
 
@@ -51,50 +57,6 @@ module Tina4
51
57
  @connection.execute("PRAGMA foreign_keys=ON")
52
58
  end
53
59
 
54
- # Resolve a SQLite URL / path against the project root (cwd).
55
- #
56
- # Convention (matches tina4-python, tina4-php, tina4-nodejs):
57
- # sqlite::memory: → :memory:
58
- # sqlite:///:memory: → :memory:
59
- # sqlite:///app.db → {cwd}/app.db (relative)
60
- # sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
61
- # sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
62
- # sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
63
- #
64
- # Never mkdir outside cwd — that was the root cause of the
65
- # "Read-only file system: '/data'" crash on macOS.
66
- def self.resolve_path(connection_string)
67
- return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
68
-
69
- # Strip the scheme + up to three slashes, preserving a potential fourth
70
- # slash (absolute) or drive letter.
71
- # `sqlite3:` is a documented alias for `sqlite:`. Normalise it FIRST or
72
- # none of the strips below match and `raw` keeps the whole connection
73
- # string, so the database file is literally named "sqlite3:app.db".
74
- # Not merely ugly: a colon is an illegal filename character on Windows,
75
- # so the documented alias is unusable there. DatabaseUrl already
76
- # normalises it; this method duplicates the strip instead of calling
77
- # it, which is how the two drifted.
78
- normalised = connection_string.sub(/^sqlite3:/, "sqlite:")
79
- raw = normalised.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
80
- return ":memory:" if raw == ":memory:"
81
-
82
- is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
83
- is_unix_abs = raw.start_with?("/")
84
-
85
- if is_windows_abs || is_unix_abs
86
- # Absolute — trust the user; don't auto-mkdir outside cwd.
87
- raw
88
- else
89
- # Relative — resolve under cwd; auto-mkdir parent dir.
90
- resolved = File.join(Dir.pwd, raw)
91
- parent = File.dirname(resolved)
92
- require "fileutils"
93
- FileUtils.mkdir_p(parent) unless File.directory?(parent)
94
- resolved
95
- end
96
- end
97
-
98
60
  def close
99
61
  @connection&.close
100
62
  end
@@ -227,43 +189,6 @@ module Tina4
227
189
  def identifier?(str)
228
190
  str.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
229
191
  end
230
-
231
- # Symbolize the keys of a whole result set, computing the key mapping ONCE.
232
- #
233
- # results_as_hash=true hands back string-keyed hashes; every row of a query
234
- # has the same columns, so the string->symbol conversion is identical for
235
- # every row. The old code called `symbolize_keys` per row, which ran
236
- # `k.to_s.to_sym` and an `is_a?` guard PER CELL: a 5,000-row x 6-column
237
- # fetch did 30,000 conversions for 6 distinct keys and made Tina4 the
238
- # slowest of the four frameworks at bulk reads (Select ALL 5,000 rows:
239
- # 10.16ms vs raw sqlite3 2.56ms). Hoisting the mapping out of the loop
240
- # roughly halves the hydration cost with byte-identical output.
241
- #
242
- # The is_a?(String|Symbol) guard is preserved: older sqlite3 gems put
243
- # positional Integer keys in the hash alongside the string names, and those
244
- # must still be dropped.
245
- def symbolize_rows(rows)
246
- return rows if rows.empty?
247
-
248
- str_keys = rows.first.keys.select { |k| k.is_a?(String) || k.is_a?(Symbol) }
249
- sym_keys = str_keys.map(&:to_sym)
250
- count = str_keys.length
251
- rows.map do |row|
252
- out = {}
253
- i = 0
254
- while i < count
255
- out[sym_keys[i]] = row[str_keys[i]]
256
- i += 1
257
- end
258
- out
259
- end
260
- end
261
-
262
- # Single-hash convenience kept for any external caller; delegates to the
263
- # batch path so there is one implementation of the mapping rule.
264
- def symbolize_keys(hash)
265
- symbolize_rows([hash]).first
266
- end
267
192
  end
268
193
  end
269
194
  end
@@ -0,0 +1,54 @@
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
+ module Tina4
9
+ # Shared row/key symbolisation for the two SQLite classes.
10
+ #
11
+ # `symbolize_rows`/`symbolize_keys` used to be byte-identical copy-paste in
12
+ # Tina4::Drivers::SqliteDriver and Tina4::Adapters::Sqlite3Adapter (the
13
+ # adapter's own comment pointed at the driver's copy). This module is the
14
+ # single home for the logic; both classes `include` it as private instance
15
+ # methods, so both share ONE implementation and can never drift again.
16
+ #
17
+ # The mapping is computed ONCE per query rather than per cell: the per-row
18
+ # form ran `k.to_s.to_sym` and an `is_a?` guard PER CELL, so a 5,000-row x
19
+ # 6-column fetch did 30,000 conversions for 6 distinct keys and made Tina4 the
20
+ # slowest of the four frameworks at bulk reads (Select ALL 5,000 rows:
21
+ # 10.16ms vs raw sqlite3 2.56ms). Hoisting the mapping out of the loop roughly
22
+ # halves the hydration cost with byte-identical output.
23
+ #
24
+ # The is_a?(String|Symbol) guard is preserved: older sqlite3 gems put
25
+ # positional Integer keys in the hash alongside the string names, and those
26
+ # must still be dropped.
27
+ module RowSymbolizer
28
+ private
29
+
30
+ # Symbolize a whole result set's keys, computing the mapping once per query.
31
+ def symbolize_rows(rows)
32
+ return rows if rows.empty?
33
+
34
+ str_keys = rows.first.keys.select { |k| k.is_a?(String) || k.is_a?(Symbol) }
35
+ sym_keys = str_keys.map(&:to_sym)
36
+ count = str_keys.length
37
+ rows.map do |row|
38
+ out = {}
39
+ i = 0
40
+ while i < count
41
+ out[sym_keys[i]] = row[str_keys[i]]
42
+ i += 1
43
+ end
44
+ out
45
+ end
46
+ end
47
+
48
+ # Single-hash convenience; delegates to the batch path so there is one
49
+ # implementation of the mapping rule.
50
+ def symbolize_keys(hash)
51
+ symbolize_rows([hash]).first
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
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
+ require "fileutils"
9
+
10
+ module Tina4
11
+ # Shared SQLite database-path resolution for the two SQLite resolvers.
12
+ #
13
+ # `resolve_path` used to be copy-pasted into Tina4::Drivers::SqliteDriver and
14
+ # Tina4::Adapters::Sqlite3Adapter with drifted scheme-stripping. This module is
15
+ # the single home for the logic; both classes `extend` it, so both keep a
16
+ # `.resolve_path` class method backed by ONE implementation and can never
17
+ # drift again.
18
+ #
19
+ # Convention (matches tina4-python, tina4-php, tina4-nodejs):
20
+ # sqlite::memory: → :memory:
21
+ # sqlite:///:memory: → :memory:
22
+ # sqlite:///app.db → {cwd}/app.db (relative)
23
+ # sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
24
+ # sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
25
+ # sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
26
+ #
27
+ # `sqlite3:` is a documented alias for `sqlite:` and is normalised first.
28
+ # Never mkdir outside cwd — that was the root cause of the
29
+ # "Read-only file system: '/data'" crash on macOS.
30
+ module SqlitePathResolver
31
+ # Resolve a SQLite URL / path against the project root (cwd).
32
+ #
33
+ # @param connection_string [String] a sqlite: / sqlite3: URL or bare path
34
+ # @return [String] the resolved path to hand to the driver, or ":memory:"
35
+ def resolve_path(connection_string)
36
+ return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
37
+
38
+ # `sqlite3:` is a documented alias for `sqlite:`; normalise it FIRST or the
39
+ # strips below miss and the file ends up literally named "sqlite3:app.db"
40
+ # (and a colon is an illegal filename character on Windows).
41
+ normalised = connection_string.to_s.sub(/^sqlite3:/, "sqlite:")
42
+ raw = normalised.sub(%r{^sqlite:///}, "").sub(%r{^sqlite://}, "").sub(/^sqlite:/, "")
43
+ return ":memory:" if raw == ":memory:"
44
+
45
+ is_windows_abs = raw.match?(%r{^[A-Za-z]:[/\\]})
46
+ is_unix_abs = raw.start_with?("/")
47
+
48
+ if is_windows_abs || is_unix_abs
49
+ # Absolute — trust the user; don't auto-mkdir outside cwd.
50
+ raw
51
+ else
52
+ # Relative — resolve under cwd; create the parent (mode 0775) ONLY when
53
+ # it stays inside cwd (ADR-0086). A relative path whose parent escapes
54
+ # the project (e.g. "../../etc/foo.db") is REFUSED loudly here — never
55
+ # the silent mkdir outside the project that used to happen.
56
+ resolved = File.join(Dir.pwd, raw)
57
+ parent = File.dirname(resolved)
58
+ expanded_parent = File.expand_path(parent)
59
+ cwd = File.expand_path(Dir.pwd)
60
+ unless expanded_parent == cwd || expanded_parent.start_with?(cwd + File::SEPARATOR)
61
+ raise ArgumentError,
62
+ "SQLite path #{connection_string.inspect} resolves outside the project " \
63
+ "directory: #{expanded_parent.inspect} is not within #{cwd.inspect}. " \
64
+ "Tina4 refuses to create directories outside the project (ADR-0086). " \
65
+ "Use an absolute path for a database that lives outside the project."
66
+ end
67
+ FileUtils.mkdir_p(parent, mode: 0o775) unless File.directory?(parent)
68
+ resolved
69
+ end
70
+ end
71
+ end
72
+ end
data/lib/tina4/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.139"
10
+ VERSION = "3.13.140"
11
11
  end
data/lib/tina4.rb CHANGED
@@ -12,6 +12,8 @@ require_relative "tina4/constants"
12
12
  require_relative "tina4/log"
13
13
  require_relative "tina4/debug" # backward compat alias
14
14
  require_relative "tina4/env"
15
+ require_relative "tina4/sqlite_path_resolver"
16
+ require_relative "tina4/row_symbolizer"
15
17
  require_relative "tina4/router"
16
18
  require_relative "tina4/request"
17
19
  require_relative "tina4/response"
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.139
4
+ version: 3.13.140
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-09-26 00:00:00.000000000 Z
11
+ date: 2026-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
@@ -327,6 +327,7 @@ files:
327
327
  - lib/tina4/response.rb
328
328
  - lib/tina4/response_cache.rb
329
329
  - lib/tina4/router.rb
330
+ - lib/tina4/row_symbolizer.rb
330
331
  - lib/tina4/secret_file.rb
331
332
  - lib/tina4/seeder.rb
332
333
  - lib/tina4/service.rb
@@ -344,6 +345,7 @@ files:
344
345
  - lib/tina4/smtp_client.rb
345
346
  - lib/tina4/sql_translator.rb
346
347
  - lib/tina4/sqlite3_gem.rb
348
+ - lib/tina4/sqlite_path_resolver.rb
347
349
  - lib/tina4/sso.rb
348
350
  - lib/tina4/ssrf.rb
349
351
  - lib/tina4/swagger.rb