rage-rb 1.19.0 → 1.19.2

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: 847e3debe188b3b34ffdd6b7306edeca0a053b82c0e5106b3faa37a6e43f20bf
4
- data.tar.gz: 202db65e0a17d27ae94178d93ddfca940a95237450095b06c0fa4e080a6fd09a
3
+ metadata.gz: 9c49338e5b69de7d3c5667af08785441a4988710da64e84946023a660cc31560
4
+ data.tar.gz: 841f4c508fb3d0739fd912967d370f5b40e69dbe8089727ea8cc56b123abd9ff
5
5
  SHA512:
6
- metadata.gz: 74610482cca2fd1394e3ffd4bd2cf577ed78a980140e39bf359ece4050bed7497dc1845a472a3c403a6fb0cbd48c1124a1714b8e7c8352b0ed720287177418c9
7
- data.tar.gz: cd7cec704b0019012d1a08aea63b97dd83e0a6ee17155228d9517575e7882482c58b4b227d6663b053cc77a2edc551e34da53d0bf9bc0463e254ca917f9e8af4
6
+ metadata.gz: 73c04963af90446e29b52564020c42048735a3df8a17e071dd744f139094e68a64cabf66396433b05dd1cd51fa1b91374fe06133754b2d802171641cf2f44956
7
+ data.tar.gz: d7561754ef307415e7d848e9ac0fc2731c03281285664eafd3c2865ebc7234bb03826c22cb803e05d48aaa3c4fce05d568a13b199f4d5ee5eb8d2613ba13c4b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.19.2] - 2025-01-06
4
+
5
+ ### Changed
6
+
7
+ - Compatibility with Rack 3 (#193).
8
+
9
+ ## [1.19.1] - 2025-12-26
10
+
11
+ ### Changed
12
+
13
+ - Use app-specific cookie keys for sessions (#189).
14
+
3
15
  ## [1.19.0] - 2025-12-03
4
16
 
5
17
  ### Added
@@ -1,5 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # `Rage::Cable` provides built-in WebSocket support for Rage apps, similar to Action Cable in Rails. It lets you mount a separate WebSocket application, define channels and connections, subscribe clients to named streams, and broadcast messages in real time.
5
+ #
6
+ # Define a channel:
7
+ # ```ruby
8
+ # class ChatChannel < Rage::Cable::Channel
9
+ # def subscribed
10
+ # stream_from "chat"
11
+ # end
12
+ #
13
+ # def receive(data)
14
+ # puts "Received message: #{data['message']}"
15
+ # end
16
+ # end
17
+ # ```
18
+ #
19
+ # Mount the Cable application:
20
+ # ```ruby
21
+ # Rage.routes.draw do
22
+ # mount Rage::Cable.application, at: "/cable"
23
+ # end
24
+ # ```
25
+ #
26
+ # Broadcast a message to a stream:
27
+ # ```ruby
28
+ # Rage.cable.broadcast("chat", { message: "Hello, world!" })
29
+ # ```
30
+ #
3
31
  module Rage::Cable
4
32
  # Create a new Cable application.
5
33
  #
@@ -19,7 +47,7 @@ module Rage::Cable
19
47
  env["rack.upgrade"] = handler
20
48
  accept_response
21
49
  else
22
- [426, { "Connection" => "Upgrade", "Upgrade" => "websocket" }, []]
50
+ [426, { "connection" => "upgrade", "upgrade" => "websocket" }, []]
23
51
  end
24
52
  end
25
53
 
@@ -203,6 +203,14 @@ class Rage::Configuration
203
203
  end
204
204
  # @!endgroup
205
205
 
206
+ # @!group Session Configuration
207
+ # Allows configuring session settings.
208
+ # @return [Rage::Configuration::Session]
209
+ def session
210
+ @session ||= Session.new
211
+ end
212
+ # @!endgroup
213
+
206
214
  # @private
207
215
  def internal
208
216
  @internal ||= Internal.new
@@ -773,6 +781,17 @@ class Rage::Configuration
773
781
  end
774
782
  end
775
783
 
784
+ class Session
785
+ # @!attribute key
786
+ # Specify the name of the session cookie.
787
+ # @return [String]
788
+ # @example Change the session cookie name
789
+ # Rage.configure do
790
+ # config.session.key = "_myapp_session"
791
+ # end
792
+ attr_accessor :key
793
+ end
794
+
776
795
  # @private
777
796
  class Internal
778
797
  attr_accessor :rails_mode
@@ -585,7 +585,7 @@ class RageController::API
585
585
 
586
586
  # Render an HTTP header requesting the client to send a Bearer token for authentication.
587
587
  def request_http_token_authentication
588
- headers["Www-Authenticate"] = "Token"
588
+ headers["www-authenticate"] = "Token"
589
589
  render plain: "HTTP Token: Access denied.", status: 401
590
590
  end
591
591
 
data/lib/rage/cookies.rb CHANGED
@@ -12,6 +12,92 @@ if !defined?(DomainName)
12
12
  ERR
13
13
  end
14
14
 
15
+ ##
16
+ # Cookies provide a convenient way to store small amounts of data on the client side that persists across requests.
17
+ # They are commonly used for session management, personalization, and tracking user preferences.
18
+ #
19
+ # Rage cookies support both simple string-based cookies and encrypted cookies for sensitive data.
20
+ #
21
+ # To use cookies, add the `domain_name` gem to your `Gemfile`:
22
+ #
23
+ # ```bash
24
+ # bundle add domain_name
25
+ # ```
26
+ #
27
+ # Additionally, if you need to use encrypted cookies, see {Session} for setup steps.
28
+ #
29
+ # ## Usage
30
+ #
31
+ # ### Basic Cookies
32
+ #
33
+ # Read and write simple string values:
34
+ #
35
+ # ```ruby
36
+ # # Set a cookie
37
+ # cookies[:user_name] = "Alice"
38
+ #
39
+ # # Read a cookie
40
+ # cookies[:user_name] # => "Alice"
41
+ #
42
+ # # Delete a cookie
43
+ # cookies.delete(:user_name)
44
+ # ```
45
+ #
46
+ # ### Cookie Options
47
+ #
48
+ # Set cookies with additional options for security and control:
49
+ #
50
+ # ```ruby
51
+ # cookies[:user_id] = {
52
+ # value: "12345",
53
+ # expires: 1.year.from_now,
54
+ # secure: true,
55
+ # httponly: true,
56
+ # same_site: :lax
57
+ # }
58
+ # ```
59
+ #
60
+ # ### Encrypted Cookies
61
+ #
62
+ # Store sensitive data securely with automatic encryption:
63
+ #
64
+ # ```ruby
65
+ # # Set an encrypted cookie
66
+ # cookies.encrypted[:api_token] = "secret-token"
67
+ #
68
+ # # Read an encrypted cookie
69
+ # cookies.encrypted[:api_token] # => "secret-token"
70
+ #
71
+ # ```
72
+ #
73
+ # ### Permanent Cookies
74
+ #
75
+ # Create cookies that expire 20 years from now:
76
+ #
77
+ # ```ruby
78
+ # cookies.permanent[:remember_token] = "token-value"
79
+ #
80
+ # # Can be combined with encrypted
81
+ # cookies.permanent.encrypted[:user_id] = current_user.id
82
+ # ```
83
+ #
84
+ # ### Domain Configuration
85
+ #
86
+ # Control which domains can access your cookies:
87
+ #
88
+ # ```ruby
89
+ # # Specific domain
90
+ # cookies[:cross_domain] = { value: "data", domain: "example.com" }
91
+ #
92
+ # # All subdomains
93
+ # cookies[:shared] = { value: "data", domain: :all }
94
+ #
95
+ # # Multiple allowed domains
96
+ # cookies[:limited] = { value: "data", domain: ["app.example.com", "api.example.com"] }
97
+ # ```
98
+ #
99
+ # @see Session
100
+ #
15
101
  class Rage::Cookies
16
102
  # @private
17
103
  def initialize(env, headers)
@@ -45,12 +131,8 @@ class Rage::Cookies
45
131
  # @param path [String]
46
132
  # @param domain [String]
47
133
  def delete(key, path: "/", domain: nil)
48
- @headers.compare_by_identity
49
-
50
134
  @request_cookies[key] = nil
51
- @headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, {
52
- value: "", expires: Time.at(0), path: path, domain: domain
53
- })
135
+ Rack::Utils.delete_cookie_header!(@headers, key, { path: path, domain: domain })
54
136
  end
55
137
 
56
138
  # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them
@@ -88,19 +170,17 @@ class Rage::Cookies
88
170
  # @example
89
171
  # cookie[:user_id] = { value: current_user.id, httponly: true, secure: true }
90
172
  def []=(key, value)
91
- @headers.compare_by_identity
92
-
93
173
  unless value.is_a?(Hash)
94
174
  serialized_value = @jar.dump(value)
95
175
  @request_cookies[key] = serialized_value
96
- @headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, { value: serialized_value, expires: @expires })
176
+ Rack::Utils.set_cookie_header!(@headers, key, { value: serialized_value, expires: @expires })
97
177
  return
98
178
  end
99
179
 
100
180
  if (domain = value[:domain])
101
181
  host = @env["HTTP_HOST"]
102
182
 
103
- _domain = if domain.is_a?(String)
183
+ processed_domain = if domain.is_a?(String)
104
184
  domain
105
185
  elsif domain == :all
106
186
  DomainName(host).domain
@@ -110,18 +190,13 @@ class Rage::Cookies
110
190
  end
111
191
 
112
192
  serialized_value = @jar.dump(value[:value])
113
- cookie = Rack::Utils.add_cookie_to_header(nil, key, {
114
- path: value[:path],
115
- secure: value[:secure],
116
- expires: value[:expires] || @expires,
117
- httponly: value[:httponly],
118
- same_site: value[:same_site],
193
+ Rack::Utils.set_cookie_header!(@headers, key, {
194
+ **value,
119
195
  value: serialized_value,
120
- domain: _domain
196
+ domain: processed_domain,
197
+ expires: value[:expires] || @expires
121
198
  })
122
-
123
199
  @request_cookies[key] = serialized_value
124
- @headers[set_cookie_key(key)] = cookie
125
200
  end
126
201
 
127
202
  def inspect
@@ -154,11 +229,6 @@ class Rage::Cookies
154
229
  @request_cookies
155
230
  end
156
231
 
157
- def set_cookie_key(key)
158
- @set_cookie_keys ||= Hash.new { |hash, key| hash[key] = "Set-Cookie".dup }
159
- @set_cookie_keys[key]
160
- end
161
-
162
232
  protected
163
233
 
164
234
  attr_writer :jar, :expires
@@ -190,10 +260,13 @@ class Rage::Cookies
190
260
  begin
191
261
  box.decrypt(Base64.urlsafe_decode64(value).byteslice(2..))
192
262
  rescue ArgumentError
263
+ Rage.logger.debug("Failed to decode encrypted cookie")
193
264
  nil
194
265
  rescue RbNaCl::CryptoError
266
+ Rage.logger.debug("Failed to decrypt encrypted cookie")
195
267
  i ||= 0
196
268
  if (box = fallback_boxes[i])
269
+ Rage.logger.debug("Trying to decrypt with fallback key ##{i + 1}")
197
270
  i += 1
198
271
  retry
199
272
  end
@@ -209,13 +209,7 @@ class Rage::Logger
209
209
  RUBY
210
210
  elsif @external_logger.is_a?(External::Dynamic)
211
211
  # a callable object is used as a logger
212
- call_method = if @external_logger.wrapped.is_a?(Proc)
213
- @external_logger.wrapped
214
- else
215
- @external_logger.wrapped.method(:call)
216
- end
217
-
218
- parameters = Rage::Internal.build_arguments(call_method, {
212
+ parameters = Rage::Internal.build_arguments(@external_logger.wrapped, {
219
213
  severity: ":#{level_name}",
220
214
  tags: "logger[:tags].freeze",
221
215
  context: "logger[:context].freeze",
@@ -14,20 +14,20 @@ class Rage::Cors
14
14
  end
15
15
 
16
16
  response = @app.call(env)
17
- response[1]["Access-Control-Allow-Credentials"] = @allow_credentials if @allow_credentials
18
- response[1]["Access-Control-Expose-Headers"] = @expose_headers if @expose_headers
17
+ response[1]["access-control-allow-credentials"] = @allow_credentials if @allow_credentials
18
+ response[1]["access-control-expose-headers"] = @expose_headers if @expose_headers
19
19
 
20
20
  response
21
21
  ensure
22
22
  if !$! && (origin = @cors_check.call(env))
23
23
  headers = response[1]
24
- headers["Access-Control-Allow-Origin"] = origin
24
+ headers["access-control-allow-origin"] = origin
25
25
  if @origins != "*"
26
- vary = headers["Vary"]
26
+ vary = headers["vary"]
27
27
  if vary.nil?
28
- headers["Vary"] = "Origin"
28
+ headers["vary"] = "Origin"
29
29
  elsif vary != "Origin"
30
- headers["Vary"] += ", Origin"
30
+ headers["vary"] += ", Origin"
31
31
  end
32
32
  end
33
33
  end
@@ -98,21 +98,21 @@ class Rage::Cors
98
98
 
99
99
  def create_headers
100
100
  headers = {
101
- "Access-Control-Allow-Origin" => "",
102
- "Access-Control-Allow-Methods" => @methods
101
+ "access-control-allow-origin" => "",
102
+ "access-control-allow-methods" => @methods
103
103
  }
104
104
 
105
105
  if @allow_headers
106
- headers["Access-Control-Allow-Headers"] = @allow_headers
106
+ headers["access-control-allow-headers"] = @allow_headers
107
107
  end
108
108
  if @expose_headers
109
- headers["Access-Control-Expose-Headers"] = @expose_headers
109
+ headers["access-control-expose-headers"] = @expose_headers
110
110
  end
111
111
  if @max_age
112
- headers["Access-Control-Max-Age"] = @max_age
112
+ headers["access-control-max-age"] = @max_age
113
113
  end
114
114
  if @allow_credentials
115
- headers["Access-Control-Allow-Credentials"] = @allow_credentials
115
+ headers["access-control-allow-credentials"] = @allow_credentials
116
116
  end
117
117
 
118
118
  headers
@@ -24,7 +24,7 @@ class Rage::RequestId
24
24
  def call(env)
25
25
  env["rage.request_id"] = validate_external_request_id(env["HTTP_X_REQUEST_ID"])
26
26
  response = @app.call(env)
27
- response[1]["X-Request-Id"] = env["rage.request_id"]
27
+ response[1]["x-request-id"] = env["rage.request_id"]
28
28
 
29
29
  response
30
30
  end
@@ -48,13 +48,13 @@ module Rage::OpenAPI
48
48
  spec_url = "#{scheme}://#{host}#{path}/json"
49
49
  page = ERB.new(File.read("#{__dir__}/index.html.erb")).result(binding)
50
50
 
51
- [200, { "Content-Type" => "text/html; charset=UTF-8" }, [page]]
51
+ [200, { "content-type" => "text/html; charset=UTF-8" }, [page]]
52
52
  end
53
53
  end
54
54
 
55
55
  json_app = ->(env) do
56
56
  spec = (__data_cache[[:spec, namespace]] ||= build(namespace:).to_json)
57
- [200, { "Content-Type" => "application/json" }, [spec]]
57
+ [200, { "content-type" => "application/json" }, [spec]]
58
58
  end
59
59
 
60
60
  app = ->(env) do
data/lib/rage/response.rb CHANGED
@@ -4,8 +4,8 @@ require "digest"
4
4
  require "time"
5
5
 
6
6
  class Rage::Response
7
- ETAG_HEADER = "ETag"
8
- LAST_MODIFIED_HEADER = "Last-Modified"
7
+ ETAG_HEADER = "etag"
8
+ LAST_MODIFIED_HEADER = "last-modified"
9
9
 
10
10
  # @private
11
11
  def initialize(headers, body)
data/lib/rage/session.rb CHANGED
@@ -2,9 +2,68 @@
2
2
 
3
3
  require "json"
4
4
 
5
+ ##
6
+ # Sessions securely store data between requests using cookies and are typically one of the most convenient and secure
7
+ # authentication mechanisms for browser-based clients.
8
+ #
9
+ # Rage sessions are encrypted using a secret key. This prevents clients from reading or tampering with session data.
10
+ #
11
+ # ## Setup
12
+ #
13
+ # 1. Add the required gems to your `Gemfile`:
14
+ #
15
+ # ```bash
16
+ # bundle add base64 domain_name rbnacl
17
+ # ```
18
+ #
19
+ # 2. Generate a secret key base (keep this value private and out of version control):
20
+ #
21
+ # ```bash
22
+ # ruby -r securerandom -e 'puts SecureRandom.hex(64)'
23
+ # ```
24
+ #
25
+ # 3. Configure your application to use the generated key, either via configuration:
26
+ #
27
+ # ```ruby
28
+ # Rage.configure do |config|
29
+ # config.secret_key_base = "my-secret-key"
30
+ # end
31
+ # ```
32
+ #
33
+ # or via the `SECRET_KEY_BASE` environment variable:
34
+ #
35
+ # ```bash
36
+ # export SECRET_KEY_BASE="my-secret-key"
37
+ # ```
38
+ #
39
+ # ## System Dependencies
40
+ #
41
+ # Rage sessions use libsodium (via RbNaCl) for encryption. On many Debian-based systems
42
+ # it is installed by default; if not, install it with:
43
+ #
44
+ # - Ubuntu / Debian:
45
+ #
46
+ # ```bash
47
+ # sudo apt install libsodium23
48
+ # ```
49
+ #
50
+ # - Fedora / RHEL / Amazon Linux:
51
+ #
52
+ # ```bash
53
+ # sudo yum install libsodium
54
+ # ```
55
+ #
56
+ # - macOS (using Homebrew):
57
+ #
58
+ # ```bash
59
+ # brew install libsodium
60
+ # ```
61
+ #
5
62
  class Rage::Session
6
63
  # @private
7
- KEY = Rack::RACK_SESSION.to_sym
64
+ def self.key
65
+ @key ||= Rage.config.session.key&.to_sym || :"_#{Rage.root.basename.to_s.gsub(/\W/, "_").downcase}_session"
66
+ end
8
67
 
9
68
  # @private
10
69
  def initialize(cookies)
@@ -92,13 +151,15 @@ class Rage::Session
92
151
  read_session.clear
93
152
  end
94
153
 
95
- @cookies[KEY] = { httponly: true, same_site: :lax, value: read_session.to_json }
154
+ @cookies[self.class.key] = { httponly: true, same_site: :lax, value: read_session.to_json }
96
155
  end
97
156
 
98
157
  def read_session
99
158
  @session ||= begin
100
- JSON.parse(@cookies[KEY] || "{}", symbolize_names: true)
159
+ session_value = @cookies[self.class.key] || @cookies[Rack::RACK_SESSION.to_sym] || "{}"
160
+ JSON.parse(session_value, symbolize_names: true)
101
161
  rescue JSON::ParserError
162
+ Rage.logger.debug("Failed to parse session cookie, resetting session")
102
163
  {}
103
164
  end
104
165
  end
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "1.19.0"
4
+ VERSION = "1.19.2"
5
5
  end
data/rage.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ["lib"]
29
29
 
30
30
  spec.add_dependency "thor", "~> 1.0"
31
- spec.add_dependency "rack", "~> 2.0"
31
+ spec.add_dependency "rack", "< 4"
32
32
  spec.add_dependency "rage-iodine", "~> 4.3"
33
33
  spec.add_dependency "zeitwerk", "~> 2.6"
34
34
  spec.add_dependency "rack-test", "~> 2.1"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.0
4
+ version: 1.19.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-12-03 00:00:00.000000000 Z
10
+ date: 2026-01-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: thor
@@ -27,16 +27,16 @@ dependencies:
27
27
  name: rack
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '2.0'
32
+ version: '4'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - "<"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.0'
39
+ version: '4'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rage-iodine
42
42
  requirement: !ruby/object:Gem::Requirement