tina4ruby 3.13.143 → 3.13.145

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: 641d95c3ab74eeac0ce2b273ee7c6c44ca8aaab5e7e2a06d8e668eacffc5fb28
4
- data.tar.gz: f8ad58627e7263bf560b499dc40fbd249d3267516d61b2c31696051f80b11852
3
+ metadata.gz: fdd61b88184de108dd6f4daefae312d7d26eab819ec084b88f5f257fce98167f
4
+ data.tar.gz: 336322ba4f2b6521d74af052391a0c1464310330935dd507c6a4ab2bfb5add11
5
5
  SHA512:
6
- metadata.gz: 2354cb021a1136116236d2a94f6c5be38331823eda3e4ea4b37483ac395e3324af69b8a88d55e856039fe1951bb74be6e4918505bcbd6f6b70075f9b2ef0f232
7
- data.tar.gz: f3c5de4a797f61db477db42c3a46e0f47dd132e7e2522b05d6127ee6c6d84b631215808fcb88953467308291a9754b39811f1ab8604f02fca93921b39b2ea67b
6
+ metadata.gz: e71cd5ffa73b75f87542f96e32fa4c503b38dacad2ea7444e381bd67ca94764be6a5c24f585e5cb1c4d4589b066303b02cef915effc1a8b8c92bd1b597176e6d
7
+ data.tar.gz: d0389a5a87a2911f7130647e12ee7f7b9c8476e75f8dcd824659d9f72c0099b640687a2a778f385ea322afb66dd8d62e435cd84467b788a9616868c1aa62d75d
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ 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.145 — 2026-10-03
10
+
11
+ Two fixes land together. The Api client no longer carries an application-configured header onto a different origin: credentials travel under many names (`X-Api-Key`, a custom bearer header), so on a cross-origin redirect only content-negotiation and transport headers cross, and every configured or per-call header is bound to the origin it was meant for; a same-origin redirect keeps them. And `tina4ruby migrate` now exits 1 when there is no usable database (unreachable or misconfigured), so CI no longer reads a failed migration as success. Both are proven against real dependencies, no mocks. No runtime gems.
12
+
13
+ ## 3.13.144 — 2026-10-02
14
+
15
+ HEAD responses are locked to exactly one `Content-Length` on the wire, equal to the length the GET would have sent. A HEAD emitting two differing `Content-Length` headers is malformed (RFC 7230 s3.3.2); a strict proxy (nginx) 502s it while lenient clients show 200. Ruby already writes a single, correct `Content-Length` with an empty body; this release LOCKS it with a conformance test that boots the real built-in server and reads the raw socket, because the in-process test reads a response object and cannot see the wire - the layer where tina4-php shipped `Content-Length: 0` on every routed HEAD (fixed in the same release). Also internal: the serve-debug readiness harness is unified on the `Server:`-banner identity guard across all four frameworks, and a carbon-benchmark drift (a `to_paginate` call with arguments, which ADR-0043 forbids) is fixed so `carbon_benchmarks.rb --carbon` runs clean. No runtime behaviour change in ruby. The framework still has no required runtime dependencies.
16
+
9
17
  ## 3.13.143 — 2026-10-01
10
18
 
11
19
  A fresh project's own styling now loads under the strict default Content-Security-Policy. The framework serves `default-src 'self'`, which a browser enforces by refusing every inline `<style>` and `<script>` -- so a new project rendered unstyled with its scripts blocked. The framework now mints one nonce per response, injects it into the `style-src` and `script-src` directives, and stamps it on every inline block it emits, de-inlining the `style=` and `on*=` attributes it used to rely on; a template that needs an inline block stamps `csp_nonce()` on it (ADR-0088, #96). A session's expiry now slides on activity: a read-only request that touched a stored session re-writes the merged record to move the deadline forward, so a request in flight next to a logout no longer loses the wrong state (ADR-0087, #94). Internally, the serve-debug readiness probe is identity-guarded so a foreign server on a reused ephemeral port can no longer be mistaken for the child under test -- a continuous-integration flake fix with no runtime change (#95). The framework still has no required runtime dependencies.
data/lib/tina4/api.rb CHANGED
@@ -41,6 +41,14 @@ module Tina4
41
41
  # master's _STRIP_ON_CROSS_ORIGIN.
42
42
  API_STRIP_ON_CROSS_ORIGIN = %w[authorization cookie].freeze
43
43
 
44
+ # The only headers carried onto a different origin. A caller-set header can
45
+ # hold a credential under any name (X-Api-Key, X-Auth-Token, ...), so a
46
+ # denylist cannot contain it: the headers configured on the client (and any
47
+ # per-call header, on a redirect) are bound to the origin they were meant for,
48
+ # and only these content-negotiation headers cross. Compared case-insensitively.
49
+ API_KEEP_ON_CROSS_ORIGIN = %w[user-agent accept accept-encoding accept-language
50
+ content-type content-length].freeze
51
+
44
52
  # A curated extension -> MIME map for guessing a multipart part's Content-Type
45
53
  # from its filename (Ruby core ships no mimetypes db, and pulling one in would
46
54
  # break the zero-dependency promise). tina4: a small built-in map covering the
@@ -265,7 +273,7 @@ module Tina4
265
273
  return error_response("download requires dest_path", path: nil) unless dest_path
266
274
 
267
275
  uri = build_uri(path, params)
268
- headers = @headers.dup
276
+ headers = configured_headers(uri).dup
269
277
  cookie = cookie_header
270
278
  headers["Cookie"] = cookie if @cookies_enabled && cookie
271
279
  headers = origin_headers(uri, headers)
@@ -542,11 +550,21 @@ module Tina4
542
550
  end
543
551
 
544
552
  def apply_headers(request, extra_headers)
545
- @headers.merge(extra_headers).each do |key, value|
553
+ configured_headers(request.uri).merge(extra_headers).each do |key, value|
546
554
  request[key] = value
547
555
  end
548
556
  end
549
557
 
558
+ # Headers configured on the client belong to its base origin, like the
559
+ # token: a target on another origin gets only the cross-origin-safe ones. A
560
+ # client with no base has no origin to bind them to, and sends them to the
561
+ # URL each call names.
562
+ def configured_headers(uri)
563
+ return @headers if @base_url.empty? || uri.nil? || same_origin?(uri, URI.parse(@base_url))
564
+
565
+ keep_cross_origin(@headers)
566
+ end
567
+
550
568
  # Execute the request with opt-in retry/backoff. Returns an APIResponse.
551
569
  #
552
570
  # With @max_retries > 0, a transport error (APIResponse#status == 0, the
@@ -591,7 +609,7 @@ module Tina4
591
609
  end
592
610
 
593
611
  # Perform a real HTTP round-trip via Net::HTTP, following redirects in a
594
- # bounded loop and stripping Authorization/Cookie on a cross-origin hop.
612
+ # bounded loop and carrying only API_KEEP_ON_CROSS_ORIGIN headers onto a cross-origin hop.
595
613
  # When stream_to is given, a 2xx body is streamed to that path in chunks
596
614
  # (never buffered whole) and no file is written on a non-2xx status.
597
615
  #
@@ -668,7 +686,7 @@ module Tina4
668
686
 
669
687
  if location
670
688
  new_uri = URI.join(current_uri.to_s, location)
671
- current_headers = strip_cross_origin(current_headers) unless same_origin?(current_uri, new_uri)
689
+ current_headers = keep_cross_origin(current_headers) unless same_origin?(current_uri, new_uri)
672
690
  # 301/302/303 downgrade a non-GET/HEAD to GET and drop the body
673
691
  # (matches urllib); 307/308 preserve method + body.
674
692
  if [301, 302, 303].include?(redirect_code) && !%w[GET HEAD].include?(current_method)
@@ -787,6 +805,10 @@ module Tina4
787
805
  headers.reject { |key, _| API_STRIP_ON_CROSS_ORIGIN.include?(key.to_s.downcase) }
788
806
  end
789
807
 
808
+ def keep_cross_origin(headers)
809
+ headers.select { |key, _| API_KEEP_ON_CROSS_ORIGIN.include?(key.to_s.downcase) }
810
+ end
811
+
790
812
  # Fetch a value from a Hash trying each key as both a symbol and a string.
791
813
  def result_value(result, *keys)
792
814
  keys.each do |key|
data/lib/tina4/cli.rb CHANGED
@@ -757,6 +757,19 @@ module Tina4
757
757
 
758
758
  # ── migrate ───────────────────────────────────────────────────────────
759
759
 
760
+ # The migrate commands cannot do their job without a database, so they
761
+ # fail. Tina4.initialize! logs a connection error and leaves
762
+ # Tina4.database nil, and returning here used to exit 0 -- a deploy whose
763
+ # database URL was wrong, or missing, went on as though it had migrated.
764
+ def exit_without_database
765
+ if ENV["TINA4_DATABASE_URL"].to_s.strip.empty?
766
+ warn "No database configured. Set TINA4_DATABASE_URL in your .env file."
767
+ else
768
+ warn "Database unavailable: TINA4_DATABASE_URL is set but could not be opened (see the error above)."
769
+ end
770
+ exit 1
771
+ end
772
+
760
773
  def cmd_migrate(argv)
761
774
  options = {}
762
775
  parser = OptionParser.new do |opts|
@@ -769,11 +782,7 @@ module Tina4
769
782
  require_relative "../tina4"
770
783
  Tina4.initialize!(Dir.pwd)
771
784
 
772
- db = Tina4.database
773
- unless db
774
- puts "No database configured. Set TINA4_DATABASE_URL in your .env file."
775
- return
776
- end
785
+ db = Tina4.database || exit_without_database
777
786
 
778
787
  migration = Tina4::Migration.new(db)
779
788
 
@@ -856,11 +865,7 @@ module Tina4
856
865
  require_relative "../tina4"
857
866
  Tina4.initialize!(Dir.pwd)
858
867
 
859
- db = Tina4.database
860
- unless db
861
- puts "No database configured. Set TINA4_DATABASE_URL in your .env file."
862
- return
863
- end
868
+ db = Tina4.database || exit_without_database
864
869
 
865
870
  migration = Tina4::Migration.new(db)
866
871
  info = migration.status
@@ -899,11 +904,7 @@ module Tina4
899
904
  require_relative "../tina4"
900
905
  Tina4.initialize!(Dir.pwd)
901
906
 
902
- db = Tina4.database
903
- unless db
904
- puts "No database configured. Set TINA4_DATABASE_URL in your .env file."
905
- return
906
- end
907
+ db = Tina4.database || exit_without_database
907
908
 
908
909
  migration = Tina4::Migration.new(db)
909
910
  results = migration.rollback(options[:steps])
data/lib/tina4/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.143"
10
+ VERSION = "3.13.145"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.143
4
+ version: 3.13.145
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-01 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3