hubssolib 3.0.3 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 891c5bc596ecfdd5f9f1e9b844ac77eaa443426607d55ba16110cc194baca3a8
4
- data.tar.gz: 42ffee7f098f4c9cbd1eec9e42e2c0f2e10a700ebd85b083113b517cd6706619
3
+ metadata.gz: c6df4bbfdb7389a9ae74fd974f9aec00f80caccf38a9702514bea230563c8abf
4
+ data.tar.gz: 6714640777624dc0a90744f125218375c42ff322401bcf65dc2d730df2de4a0c
5
5
  SHA512:
6
- metadata.gz: 7d0046239b22405b6e6cf47d3e8e326bcc9f4c88a58ced4b13a3eab14fd822bb16bc63d3b06523a926abda7d46d53a15211000fdd9b7aff7e071ba2b708afd92
7
- data.tar.gz: 0a1cd9ccaab6b08ae323ad572f3a563fa002c7a2250073cb0b76848f8f2c8879350828664fe37ee9a0590d134a8949dacb4419a54520569a3468d6a5b86eb3da
6
+ metadata.gz: 52375b875cda94de7b2823a1b718a5588f27c0c093d4f0010373670ffc4b54da4c5211e627719a56f10179cb3a91b86403a31f955b07348f7bfdaa25906de410
7
+ data.tar.gz: 5ef7b400a85299a3925994bba59d8c6ff1b1ded612541affd288413955eee935971e7d0bb8e61855fef42955baa3b6b2331333b720de82f1cdfcfff0f4a5eb94
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 3.2.0, 15-Feb-2025
2
+
3
+ Introduces the user change handler mechanism, a scheme whereby an external application tells Hub about a Rake task that the application has implemented, through which Hub can inform if of changes to a user's e-mail address or "real" name. See the gem's `README.md` for details, under "Applications with an existing user model".
4
+
5
+ ## 3.1.0, 14-Feb-2025
6
+
7
+ Environment variable `HUB_IDLE_TIME_LIMIT` can be used to override the idle timeout, with a value expressed in seconds. It must be set in the environment of any application using Hub, including the Hub application itself.
8
+
1
9
  ## 3.0.3, 10-Feb-2025
2
10
 
3
11
  Change JavaScript code used for the login indicator so that simpler engines such as [Duktape](https://duktape.org) can run it. Operates correctly in script-enabled [NetSurf](https://www.netsurf-browser.org) now.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hubssolib (3.0.3)
4
+ hubssolib (3.1.0)
5
5
  base64 (~> 0.2)
6
6
  drb (~> 2.2)
7
7
 
@@ -13,7 +13,7 @@ GEM
13
13
  debug (1.10.0)
14
14
  irb (~> 1.10)
15
15
  reline (>= 0.3.8)
16
- diff-lcs (1.5.1)
16
+ diff-lcs (1.6.0)
17
17
  docile (1.4.1)
18
18
  doggo (1.4.0)
19
19
  rspec-core (~> 3.13)
@@ -52,7 +52,7 @@ GEM
52
52
  simplecov_json_formatter (~> 0.1)
53
53
  simplecov-html (0.13.1)
54
54
  simplecov_json_formatter (0.1.4)
55
- stringio (3.1.2)
55
+ stringio (3.1.3)
56
56
 
57
57
  PLATFORMS
58
58
  ruby
data/README.md CHANGED
@@ -48,13 +48,15 @@ Finally you can install the Hub application using whatever mechanism you prefer
48
48
 
49
49
  Some configuration is needed using externally set environment variables. These are actually picked up by the Hub gem but you won't know what values to set until the application, DRb server and gem are all installed.
50
50
 
51
- * `HUB_CONNECTION_URI` — as already discussed, this holds a DRb URI giving the connection socket on which the server listens and to which clients connect; it defaults to `~/.hub_drb`.
52
- * `HUB_PATH_PREFIX` — sometimes the Hub Gem redirects to various locations within the Hub application. If you have installed the application away from document root, specify the prefix to put onto redirection paths here (otherwise, provide an empty string). For example, when redirecting to the `account` controller's `login` method, the path used is `HUB_PATH_PREFIX + '/account/login'`.
53
- * `HUB_BYPASS_SSL` - normally Hub sets cookies as secure-only in Production mode, requiring `https` fetches. This isn't enforced in e.g. development mode. If you want to allow insecure transport in Production, set `HUB_BYPASS_SSL` to `true`.
51
+ * `HUB_CONNECTION_URI` — as already discussed, this holds a DRb URI giving the connection socket on which the server listens and to which clients connect; it defaults to `~/.hub_drb`.
52
+ * `HUB_PATH_PREFIX` — sometimes the Hub Gem redirects to various locations within the Hub application. If you have installed the application away from document root, specify the prefix to put onto redirection paths here (otherwise, provide an empty string). For example, when redirecting to the `account` controller's `login` method, the path used is `HUB_PATH_PREFIX + '/account/login'`.
53
+ * `HUB_BYPASS_SSL` - normally Hub sets cookies as secure-only in Production mode, requiring `https` fetches. This isn't enforced in e.g. development mode. If you want to allow insecure transport in Production, set `HUB_BYPASS_SSL` to `true`.
54
54
 
55
- Usually, these are set up in a Web server configuration file as part of launching an FCGI process to host the Hub application.
55
+ Usually, these are set up in a Web server configuration file as part of launching an FCGI process to host the Hub application. Don't forget to set up the application's `database.yml` file in the usual fashion. use `rake db:migrate` to build the empty database structure.
56
56
 
57
- Don't forget to set up the application's `database.yml` file in the usual fashion. use `rake db:migrate` to build the empty database structure.
57
+ Optional environment variables for configuration are:
58
+
59
+ * `HUB_IDLE_TIME_LIMIT` - by default Hub applies a 4 hour session idle timeout. Override by setting this variable to a number **in seconds**. This must be set equally in the environment of **all applications using Hub** including Hub itself, since it is the "beforehand" callback that checks the idle timer; this can run at any time in any of your collection of Hub-integrated applications, depending on the part of your site with which the user next interacts.
58
60
 
59
61
  ## Cookies and domains
60
62
 
@@ -105,7 +107,7 @@ For full integration with Hub, particularly when it comes to showing or hiding t
105
107
 
106
108
  Applications with no concept of user log-in are easy to integrate with Hub. Applications with only the concept of logging in for administrative purposes are similarly easy, provided your administrators do not mind having to log in using the application's own administrative mechanisms (so you basically treat the application as if it has no existing user model).
107
109
 
108
- To integrate, add the Hub filters into `application.rb` just inside the definition of the `ApplicationController` class:
110
+ To integrate, add the Hub callbacks into `application.rb` just inside the definition of the `ApplicationController` class:
109
111
 
110
112
  ```ruby
111
113
  # Hub single sign-on support.
@@ -116,20 +118,20 @@ before_action :hubssolib_beforehand
116
118
  after_action :hubssolib_afterwards</pre>
117
119
  ```
118
120
 
119
- Within any controller which has actions which you wish to protect with Hub login, define a variable `@@hubssolib_permissions` and provide an accessor method for it. I'll deal with the accessor method first; for a controller called `FooController`, add the following to `foo_controller.rb`:
121
+ Within any controller which has actions which you wish to protect with Hub login, define a constant `HUBSSOLIB_PERMISSIONS` and provide an accessor method for it. I'll deal with the accessor method first; in any controller for which Hub is to guard access, add the following:
120
122
 
121
123
  ```ruby
122
- def FooController.hubssolib_permissions
123
- @@hubssolib_permissions
124
+ def self.hubssolib_permissions
125
+ HUBSSOLIB_PERMISSIONS
124
126
  end
125
127
  ```
126
128
 
127
129
  More details are provided [below](#permissions) but, in brief, to define the permissions variable you create an instance of `HubSsoLib::Permissions`. The constructor is passed a hash. The hash keys are symbolized names of the controller actions you want to protect. The hash values are an array of privileges required to access the action, from a choice of one or more of `:admin`, `:webmaster`, `:privileged` and `:normal`. These relate to the roles you can assign to accounts as Hub administrator. For example:
128
130
 
129
131
  ```ruby
130
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
131
- :show => [ :admin, :webmaster, :privileged, :normal ],
132
- :edit => [ :admin, :webmaster ]
132
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
133
+ show: [ :admin, :webmaster, :privileged, :normal ],
134
+ edit: [ :admin, :webmaster ]
133
135
  })
134
136
  ```
135
137
 
@@ -138,22 +140,61 @@ In this example, any user can access the controller's `show` action but only use
138
140
  A user's role(s) must match at least one of the privileges in the array for a given action — so even if your account has an administrator role (and _only_ an administrator role), it won't be able to access a protected action unless `:admin` is included in the array given within the hash to the `HubSsoLib::Permissions` constructor. For example:
139
141
 
140
142
  ```ruby
141
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
142
- :weblist => [ :webmaster, :privileged ]
143
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
144
+ weblist: [ :webmaster, :privileged ]
143
145
  })
144
146
  ```
145
147
 
146
148
  Here, only accounts with the webmaster or privileged role associated can access the `weblist` action. If an account has only normal and/or administrative roles, it won't be allowed through.
147
149
 
148
150
  ### Applications with an existing user model
151
+ #### General concerns
149
152
 
150
153
  If you want to integrate Hub with an application which already has the concept of user accounts, logging in and logging out, there are two main approaches.
151
154
 
152
- * Remove the existing mechanism and replace with Hub (see above). Removal may be through actually deleting code, models and filters related to that mechanism or simply removing or blocking access to the parts of the application that deal with the users and dropping in Hub equivalents over a minimum amount of code, reducing overall changes to the application but leaving a less clean result.
155
+ * Remove the existing mechanism and replace with Hub (see above). Removal may be through actually deleting code, models and callbacks related to that mechanism or simply removing or blocking access to the parts of the application that deal with the users and dropping in Hub equivalents over a minimum amount of code, reducing overall changes to the application but leaving a less clean result.
153
156
  * Use a `before_action` in the application controller to run special code which you write, which maps a logged in Hub user to an existing application user. If the visitor is logged into Hub and no corresponding local application user account exists, one is created automatically based on the Hub account credentials.
154
157
 
155
158
  Neither approach is problem-free and both require quite a lot of effort and testing. Automated testing is very hard because the modified application's behaviour depends upon logging in or out of Hub, which is running elsewhere. Unfortunately Rails doesn't offer a universally supported single sign-on mechanism so applications all use different approaches to user management; this means that there is no magic bullet to integration with Hub. You have to learn and understand the structure of the application being integrated and be prepared to make changes that are potentially quite extensive.
156
159
 
160
+ #### Synchronisation concerns, where applicable
161
+
162
+ Some applications might use local User records for relational purposes. Suppose users could author Posts. We could simply freeze the author name with a Post using a column in that table. This means the author name of any Post is easy to display. However, if that author changed their name, only new Posts would show the new name. If the application wanted to answer a question such as, "List all Posts by a given author", it would be relatively expensive. And if an application wanted to internally keep a record of author e-mail addresses for things like e-mailed notifications of some kind, it would get worse; the author might change their e-mail address in Hub and the integrated application would not know.
163
+
164
+ To solve these problems, external applications integrated with Hub can participate in the user change handler mechanism. The Hub-integrated external application calls `HubSsoLib::Core#hubssolib_register_user_change_handler` to register an interest in Hub user alterations. The mechanism invokes an application-defined Rake task, which can update user records however it sees fit. The method is given the application's name, it's Rails application root - the filesystem location of the application, since we need to use that as a current working directory to issue a `bundle exec rake your:task:name...` - and the name of the Rake task to run. Registration is usually done in `config/application.rb` - for example:
165
+
166
+ ```ruby
167
+ module Foo
168
+ class Application < Rails::Application
169
+ require HubSsoLib::Core
170
+
171
+ hubssolib_register_user_change_handler(
172
+ app_name: Rails.application.name,
173
+ app_root: Rails.root,
174
+ task_name: 'hub:update_user'
175
+ )
176
+
177
+ # ...etc...
178
+ end
179
+ end
180
+ ```
181
+
182
+ The Rake task can have any name that works for your application. A "hub" namespace is recommended but entirely optional. If a Hub user edits their details, then the task is invoked with four positional parameters - the user's old e-mail address and old Hub unique name, as returned by `HubSsoLib::Core#hubssolib_unique_name` - followed by the new e-mail address and new unique name (at least one, or both of those will always have actually changed). The Rake task can then look up the external application's User record via the old address or unique name and, if found, update it. It should either throw an exception or `exit` with a non-zero status code if it fails to store the updated details - in that case, Hub will roll back user changes on its side and warn the end user.
183
+
184
+ For example, `lib/tasks/hub.rake` might look like this:
185
+
186
+ ```ruby
187
+ namespace :hub do
188
+ desc 'Update a user with details sent from Hub'
189
+ task :update_user, [:old_email, :old_name, :new_email, :new_name] => :environment do | t, args |
190
+ user = User.find_by_email_address(old_email)
191
+ user&.update!(email_address: new_email, display_name: new_name)
192
+ end
193
+ end
194
+ ```
195
+
196
+
197
+
157
198
  ## Hub library API
158
199
 
159
200
  The Hub component interfaces that should be used by application authors when integrating with the Hub single sign-on mechanism are described below. If you want a complete list of all public interfaces, consult the file `hub_sso_lib.rb` inside the Hub gem. All functions and classes therein are fully commented to describe the purpose of each class, along with the purpose, input parameters and return values of class methods and instance methods.
@@ -176,9 +217,9 @@ Hub protects against access to actions in controller by using a `before_action`
176
217
  Permitted roles are expressed as single symbols or their equivalent strings, or an array containing many symbols or equivalent strings. Most often, an array of symbols is used. To create a permissions object, instantiate `HubSsoLib::Permissions`. For example:
177
218
 
178
219
  ```ruby
179
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
180
- :show => [ :admin, :webmaster, :privileged, :normal ],
181
- :edit => [ :admin, :webmaster ]
220
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
221
+ show: [ :admin, :webmaster, :privileged, :normal ],
222
+ edit: [ :admin, :webmaster ]
182
223
  })
183
224
  ```
184
225
 
@@ -189,7 +230,7 @@ The above line of code typically appears at the start of the class definition fo
189
230
  ```ruby
190
231
  class AccountController < ApplicationController
191
232
 
192
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
233
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
193
234
  # ...permissions here...
194
235
  })
195
236
 
@@ -197,11 +238,11 @@ class AccountController < ApplicationController
197
238
  end
198
239
  ```
199
240
 
200
- Having created the permissions object, you need to expose variable `@@hubssolib_permissions` to Hub in a way that it understands. To do this, create an instance method called `hubssolib_permissions` that just returns the variable:
241
+ Having created the permissions object, you need to expose constant `HUBSSOLIB_PERMISSIONS` to Hub in a way that it understands. To do this, create a class method called `hubssolib_permissions` that just returns the variable:
201
242
 
202
243
  ```ruby
203
- def AccountController.hubssolib_permissions
204
- @@hubssolib_permissions
244
+ def self.hubssolib_permissions
245
+ HUBSSOLIB_PERMISSIONS
205
246
  end
206
247
  ```
207
248
 
@@ -210,12 +251,12 @@ So the full preamble in this example is:
210
251
  ```ruby
211
252
  class AccountController < ApplicationController
212
253
 
213
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
254
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
214
255
  ...permissions here...
215
256
  })
216
257
 
217
- def AccountController.hubssolib_permissions
218
- @@hubssolib_permissions
258
+ def self.hubssolib_permissions
259
+ HUBSSOLIB_PERMISSIONS
219
260
  end
220
261
 
221
262
  ...existing class contents here...
@@ -247,7 +288,7 @@ Before any action in a Hub integrated application, `hubssolib_beforehand` must b
247
288
  before_action :hubssolib_beforehand
248
289
  ```
249
290
 
250
- The filter is the core of the Hub protection mechanism, making sure that no action can run unless the user is logged in (unless the action is completely protected) and their account is associated with at least one of the roles required to access the action.
291
+ This callback is the core of the Hub protection mechanism, making sure that no action can run unless the user is logged in (unless the action is completely protected) and their account is associated with at least one of the roles required to access the action.
251
292
 
252
293
  #### The "after" action: `hubssolib_afterwards`
253
294
 
@@ -257,8 +298,6 @@ After any action in a Hub integrated application, `hubssolib_afterward` must be
257
298
  after_action :hubssolib_afterwards
258
299
  ```
259
300
 
260
- At the time of writing the filter does nothing, but is included to allow for future expansion and avoid API changes that might force application integrators to modify their code.
261
-
262
301
  #### Finding out about the current user
263
302
 
264
303
  Most Hub integration methods are geared around making it easy to find out about a currently logged in user.
data/hubssolib.gemspec CHANGED
@@ -4,7 +4,7 @@ spec = Gem::Specification.new do |s|
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.name = 'hubssolib'
6
6
 
7
- s.version = '3.0.3'
7
+ s.version = '3.2.0'
8
8
  s.author = 'Andrew Hodgkinson and others'
9
9
  s.email = 'ahodgkin@rowing.org.uk'
10
10
  s.homepage = 'http://pond.org.uk/'
data/lib/hub_sso_lib.rb CHANGED
@@ -19,6 +19,7 @@ module HubSsoLib
19
19
 
20
20
  require 'drb'
21
21
  require 'securerandom'
22
+ require 'json'
22
23
 
23
24
  # DRb connection
24
25
  HUB_CONNECTION_URI = ENV['HUB_CONNECTION_URI'] || 'drbunix:' + File.join( ENV['HOME'] || '/', '/.hub_drb')
@@ -33,13 +34,28 @@ module HubSsoLib
33
34
  raise 'Exiting'
34
35
  end
35
36
 
37
+ # External application command registry for on-user-change events
38
+ HUB_COMMAND_REGISTRY = ENV['HUB_COMMAND_REGISTRY'] || File.join( ENV['HOME'] || '/', '/.hub_cmd_reg')
39
+
40
+ unless Dir.exist?(File.dirname(HUB_COMMAND_REGISTRY))
41
+ puts
42
+ puts '*' * 80
43
+ puts "Invalid path specified by HUB_COMMAND_REGISTRY (#{ HUB_COMMAND_REGISTRY.inspect })"
44
+ puts '*' * 80
45
+ puts
46
+
47
+ raise 'Exiting'
48
+ end
49
+
36
50
  # Location of Hub application root.
51
+ #
37
52
  HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
38
53
 
39
54
  # Time limit, *in seconds*, for the account inactivity timeout.
40
55
  # If a user performs no Hub actions during this time they will
41
56
  # be automatically logged out upon their next action.
42
- HUB_IDLE_TIME_LIMIT = 4 * 60 * 60
57
+ #
58
+ HUB_IDLE_TIME_LIMIT = ENV['HUB_IDLE_TIME_LIMIT']&.to_i || 4 * 60 * 60
43
59
 
44
60
  # Shared cookie name.
45
61
  HUB_COOKIE_NAME = :hubapp_shared_id
@@ -708,9 +724,96 @@ module HubSsoLib
708
724
  user ? "#{user.user_real_name} (#{user.user_id})" : 'Anonymous'
709
725
  end
710
726
 
711
- # Main filter method to implement HubSsoLib permissions management,
712
- # session expiry and so-on. Call from controllers only, always as a
713
- # before_fitler.
727
+ # If an application needs to know about changes of a user e-mail address
728
+ # or display name (e.g. because of sync to a local relational store of
729
+ # users related to other application-managed resources, with therefore a
730
+ # desire to keep that store up to date), it can register a task to run
731
+ # on-change here. When a user edits their information, Hub runs through
732
+ # all such commands, allowing external applications to manage their own
733
+ # state with no need for coupled configuration or other duplication.
734
+ #
735
+ # The registered name must be a Rake task and the application must specify
736
+ # its location in the filesystem so that the PWD can be changed there, in
737
+ # order to execute the Rake task via "bundle exec". The task is passed the
738
+ # following parameters, in the specified order:
739
+ #
740
+ # - User's old e-mail address
741
+ # - User's old unique display name (as returned by #hubssolib_unique_name)
742
+ # - User's new e-mail address (which might be the same as the old)
743
+ # - User's old unique display name (which might be unchanged too)
744
+ #
745
+ # This is a newer Hub interface which uses named parameters rather than
746
+ # positional:
747
+ #
748
+ # +app_name+:: Application name, e.g. "beast"; make sure this is unique.
749
+ # +app_root+:: Application's Rails root, e.g. "/home/fred/rails/beast".
750
+ # +task_name+:: Rake task name, e.g. "hub:update_user".
751
+ #
752
+ # An example invocation in "config/application.rb" might look like this:
753
+ #
754
+ # module Foo
755
+ # class Application < Rails::Application
756
+ #  require HubSsoLib::Core
757
+ #
758
+ # hubssolib_register_user_change_handler(
759
+ # app_name: Rails.application.name,
760
+ # app_root: Rails.root,
761
+ # task_name: 'hub:update_user'
762
+ # )
763
+ #
764
+ # config.load_defaults 8.0 # ...etc...
765
+ # end
766
+ # end
767
+ #
768
+ def hubssolib_register_user_change_handler(app_name:, app_root:, task_name:)
769
+ File.open(HUB_COMMAND_REGISTRY, File::RDWR | File::CREAT) do |file|
770
+ file.flock(File::LOCK_EX)
771
+
772
+ commands_json = file.read()
773
+ commands_hash = (JSON.parse(commands_json) rescue nil) if commands_json.present?
774
+ commands_hash ||= {}
775
+
776
+ file.rewind()
777
+
778
+ commands_hash[app_name] = {
779
+ root: app_root,
780
+ task: task_name
781
+ }
782
+
783
+ file.write(JSON.fast_generate(commands_hash))
784
+ file.truncate(file.pos)
785
+ end
786
+ end
787
+
788
+ # Returns all change handlers registered by prior calls made to
789
+ # #hubssolib_register_user_change_handler. Returns a Hash, keyed by Rails
790
+ # application name, with values of another Hash:
791
+ #
792
+ # * +root+ => Rails application root
793
+ # * +task+ => Name of Rake task to be run
794
+ #
795
+ # All keys are Strings.
796
+ #
797
+ # This is usually called by the Hub application only, when it is processing
798
+ # a user's request to change their information.
799
+ #
800
+ def hubssolib_registered_user_change_handlers
801
+ commands_hash = {}
802
+
803
+ File.open(HUB_COMMAND_REGISTRY, File::RDWR | File::CREAT) do |file|
804
+ file.flock(File::LOCK_EX)
805
+
806
+ commands_json = file.read()
807
+ commands_hash = (JSON.parse(commands_json) rescue nil) if commands_json.present?
808
+ commands_hash ||= {}
809
+ end
810
+
811
+ return commands_hash
812
+ end
813
+
814
+ # Mandatory controller "before_action" callback method which activates
815
+ # HubSsoLib permissions management, session expiry and so-on. Usually
816
+ # invoked in ApplicationController.
714
817
  #
715
818
  def hubssolib_beforehand
716
819
 
@@ -792,7 +895,8 @@ module HubSsoLib
792
895
  end
793
896
  end
794
897
 
795
- # Main after_filter method to tidy up after running state changes.
898
+ # Mandatory controller "after_action" callback method to tidy up after Hub
899
+ # actions during a request. Usually invoked in ApplicationController.
796
900
  #
797
901
  def hubssolib_afterwards
798
902
  begin
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hubssolib
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hodgkinson and others
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-10 00:00:00.000000000 Z
10
+ date: 2025-02-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: drb