rubygems-update 3.4.10 → 3.4.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Manifest.txt +4 -0
  4. data/bundler/CHANGELOG.md +22 -0
  5. data/bundler/lib/bundler/build_metadata.rb +2 -2
  6. data/bundler/lib/bundler/definition.rb +9 -1
  7. data/bundler/lib/bundler/gem_version_promoter.rb +1 -1
  8. data/bundler/lib/bundler/lazy_specification.rb +1 -1
  9. data/bundler/lib/bundler/resolver/base.rb +1 -3
  10. data/bundler/lib/bundler/ruby_version.rb +1 -1
  11. data/bundler/lib/bundler/rubygems_ext.rb +5 -3
  12. data/bundler/lib/bundler/source/rubygems.rb +5 -8
  13. data/bundler/lib/bundler/spec_set.rb +2 -2
  14. data/bundler/lib/bundler/templates/newgem/bin/console.tt +0 -4
  15. data/bundler/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt +5 -0
  16. data/bundler/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt +1 -1
  17. data/bundler/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb +2 -2
  18. data/bundler/lib/bundler/vendor/uri/lib/uri/version.rb +1 -1
  19. data/bundler/lib/bundler/version.rb +1 -1
  20. data/bundler/lib/bundler.rb +2 -2
  21. data/lib/rubygems/command_manager.rb +2 -2
  22. data/lib/rubygems/commands/owner_command.rb +4 -2
  23. data/lib/rubygems/exceptions.rb +10 -0
  24. data/lib/rubygems/gemcutter_utilities.rb +48 -6
  25. data/lib/rubygems/installer.rb +1 -1
  26. data/lib/rubygems/request_set.rb +2 -2
  27. data/lib/rubygems/specification.rb +3 -1
  28. data/lib/rubygems/stub_specification.rb +2 -1
  29. data/lib/rubygems/webauthn_listener/response.rb +161 -0
  30. data/lib/rubygems/webauthn_listener.rb +92 -0
  31. data/lib/rubygems.rb +1 -1
  32. data/rubygems-update.gemspec +1 -1
  33. data/test/rubygems/helper.rb +14 -0
  34. data/test/rubygems/test_bundled_ca.rb +1 -1
  35. data/test/rubygems/test_config.rb +1 -1
  36. data/test/rubygems/test_deprecate.rb +1 -1
  37. data/test/rubygems/test_exit.rb +1 -1
  38. data/test/rubygems/test_gem_commands_owner_command.rb +67 -0
  39. data/test/rubygems/test_gem_commands_push_command.rb +73 -0
  40. data/test/rubygems/test_gem_commands_yank_command.rb +84 -0
  41. data/test/rubygems/test_gem_ext_cargo_builder.rb +1 -0
  42. data/test/rubygems/test_gem_gemcutter_utilities.rb +72 -4
  43. data/test/rubygems/test_kernel.rb +1 -1
  44. data/test/rubygems/test_project_sanity.rb +32 -3
  45. data/test/rubygems/test_remote_fetch_error.rb +1 -1
  46. data/test/rubygems/test_webauthn_listener.rb +120 -0
  47. data/test/rubygems/test_webauthn_listener_response.rb +93 -0
  48. data/test/rubygems/utilities.rb +43 -3
  49. metadata +7 -3
@@ -230,10 +230,77 @@ class TestGemGemcutterUtilities < Gem::TestCase
230
230
  assert_equal "111111", @fetcher.last_request["OTP"]
231
231
  end
232
232
 
233
- def util_sign_in(response, host = nil, args = [], extra_input = "")
234
- email = "you@example.com"
235
- password = "secret"
236
- profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
233
+ def test_sign_in_with_webauthn_enabled
234
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
235
+ response_fail = "You have enabled multifactor authentication"
236
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
237
+ port = 5678
238
+ server = TCPServer.new(port)
239
+
240
+ TCPServer.stub(:new, server) do
241
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
242
+ util_sign_in(proc do
243
+ @call_count ||= 0
244
+ if (@call_count += 1).odd?
245
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
246
+ else
247
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
248
+ end
249
+ end, nil, [], "", webauthn_verification_url)
250
+ end
251
+ ensure
252
+ server.close
253
+ end
254
+
255
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
256
+ assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output
257
+ assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
258
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
259
+ end
260
+
261
+ def test_sign_in_with_webauthn_enabled_with_error
262
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
263
+ response_fail = "You have enabled multifactor authentication"
264
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
265
+ port = 5678
266
+ server = TCPServer.new(port)
267
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
268
+
269
+ error = assert_raise Gem::MockGemUi::TermError do
270
+ TCPServer.stub(:new, server) do
271
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
272
+ util_sign_in(proc do
273
+ @call_count ||= 0
274
+ if (@call_count += 1).odd?
275
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
276
+ else
277
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
278
+ end
279
+ end, nil, [], "", webauthn_verification_url)
280
+ end
281
+ ensure
282
+ server.close
283
+ end
284
+ end
285
+ assert_equal 1, error.exit_code
286
+
287
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
288
+ assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output
289
+ assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error
290
+ refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
291
+ refute_match "Signed in with API key:", @sign_in_ui.output
292
+ end
293
+
294
+ def util_sign_in(response, host = nil, args = [], extra_input = "", webauthn_url = nil)
295
+ email = "you@example.com"
296
+ password = "secret"
297
+ profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
298
+ webauthn_response =
299
+ if webauthn_url
300
+ HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK")
301
+ else
302
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
303
+ end
237
304
 
238
305
  if host
239
306
  ENV["RUBYGEMS_HOST"] = host
@@ -244,6 +311,7 @@ class TestGemGemcutterUtilities < Gem::TestCase
244
311
  @fetcher = Gem::FakeFetcher.new
245
312
  @fetcher.data["#{host}/api/v1/api_key"] = response
246
313
  @fetcher.data["#{host}/api/v1/profile/me.yaml"] = profile_response
314
+ @fetcher.data["#{host}/api/v1/webauthn_verification"] = webauthn_response
247
315
  Gem::RemoteFetcher.fetcher = @fetcher
248
316
 
249
317
  @sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n" + extra_input)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "helper"
3
3
 
4
- class TestKernel < Gem::TestCase
4
+ class TestGemKernel < Gem::TestCase
5
5
  def setup
6
6
  super
7
7
 
@@ -3,13 +3,36 @@
3
3
  require_relative "helper"
4
4
  require "open3"
5
5
 
6
- class TestProjectSanity < Gem::TestCase
6
+ class TestGemProjectSanity < Gem::TestCase
7
+ def setup
8
+ end
9
+
10
+ def teardown
11
+ end
12
+
7
13
  def test_manifest_is_up_to_date
8
- pend unless File.exist?(File.expand_path("../../Rakefile", __dir__))
14
+ pend unless File.exist?("#{root}/Rakefile")
9
15
 
10
16
  _, status = Open3.capture2e("rake check_manifest")
11
17
 
12
- assert status.success?, "Expected Manifest.txt to be up to date, but it's not. Run `rake update_manifest` to sync it."
18
+ unless status.success?
19
+ original_contents = File.read("#{root}/Manifest.txt")
20
+
21
+ # Update the manifest to see if it fixes the problem
22
+ Open3.capture2e("rake update_manifest")
23
+
24
+ out, status = Open3.capture2e("rake check_manifest")
25
+
26
+ # If `rake update_manifest` fixed the problem, that was the original
27
+ # issue, otherwise it was an unknown error, so print the error output
28
+ if status.success?
29
+ File.write("#{root}/Manifest.txt", original_contents)
30
+
31
+ raise "Expected Manifest.txt to be up to date, but it's not. Run `rake update_manifest` to sync it."
32
+ else
33
+ raise "There was an error running `rake check_manifest`: #{out}"
34
+ end
35
+ end
13
36
  end
14
37
 
15
38
  def test_require_rubygems_package
@@ -17,4 +40,10 @@ class TestProjectSanity < Gem::TestCase
17
40
 
18
41
  assert status.success?, err
19
42
  end
43
+
44
+ private
45
+
46
+ def root
47
+ File.expand_path("../..", __dir__)
48
+ end
20
49
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "helper"
3
3
 
4
- class TestRemoteFetchError < Gem::TestCase
4
+ class TestGemRemoteFetchError < Gem::TestCase
5
5
  def test_password_redacted
6
6
  error = Gem::RemoteFetcher::FetchError.new("There was an error fetching", "https://user:secret@gemsource.org")
7
7
  refute_match %r{secret}, error.to_s
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener"
5
+
6
+ class WebauthnListenerTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @server = TCPServer.new 0
10
+ @port = @server.addr[1].to_s
11
+ end
12
+
13
+ def test_wait_for_otp_code_get_follows_options
14
+ wait_for_otp_code
15
+ assert Gem::MockBrowser.options(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPNoContent
16
+ assert Gem::MockBrowser.get(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPOK
17
+ end
18
+
19
+ def test_wait_for_otp_code_options_request
20
+ wait_for_otp_code
21
+ response = Gem::MockBrowser.options URI("http://localhost:#{@port}?code=xyz")
22
+
23
+ assert response.is_a? Net::HTTPNoContent
24
+ assert_equal Gem.host, response["access-control-allow-origin"]
25
+ assert_equal "POST", response["access-control-allow-methods"]
26
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
27
+ assert_equal "close", response["Connection"]
28
+ end
29
+
30
+ def test_wait_for_otp_code_get_request
31
+ wait_for_otp_code
32
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?code=xyz")
33
+
34
+ assert response.is_a? Net::HTTPOK
35
+ assert_equal "text/plain", response["Content-Type"]
36
+ assert_equal "7", response["Content-Length"]
37
+ assert_equal Gem.host, response["access-control-allow-origin"]
38
+ assert_equal "POST", response["access-control-allow-methods"]
39
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
40
+ assert_equal "close", response["Connection"]
41
+ assert_equal "success", response.body
42
+
43
+ @thread.join
44
+ assert_equal "xyz", @thread[:otp]
45
+ end
46
+
47
+ def test_wait_for_otp_code_invalid_post_req_method
48
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Invalid HTTP method POST received.")
49
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}?code=xyz")
50
+
51
+ assert response
52
+ assert response.is_a? Net::HTTPMethodNotAllowed
53
+ assert_equal "GET, OPTIONS", response["allow"]
54
+ assert_equal "close", response["Connection"]
55
+
56
+ @thread.join
57
+ assert_nil @thread[:otp]
58
+ end
59
+
60
+ def test_wait_for_otp_code_incorrect_path
61
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Page at /path not found.")
62
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}/path?code=xyz")
63
+
64
+ assert response.is_a? Net::HTTPNotFound
65
+ assert_equal "close", response["Connection"]
66
+
67
+ @thread.join
68
+ assert_nil @thread[:otp]
69
+ end
70
+
71
+ def test_wait_for_otp_code_no_params_response
72
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
73
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}")
74
+
75
+ assert response.is_a? Net::HTTPBadRequest
76
+ assert_equal "text/plain", response["Content-Type"]
77
+ assert_equal "22", response["Content-Length"]
78
+ assert_equal "close", response["Connection"]
79
+ assert_equal "missing code parameter", response.body
80
+
81
+ @thread.join
82
+ assert_nil @thread[:otp]
83
+ end
84
+
85
+ def test_wait_for_otp_code_incorrect_params
86
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
87
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?param=xyz")
88
+
89
+ assert response.is_a? Net::HTTPBadRequest
90
+ assert_equal "text/plain", response["Content-Type"]
91
+ assert_equal "22", response["Content-Length"]
92
+ assert_equal "close", response["Connection"]
93
+ assert_equal "missing code parameter", response.body
94
+
95
+ @thread.join
96
+ assert_nil @thread[:otp]
97
+ end
98
+
99
+ private
100
+
101
+ def wait_for_otp_code
102
+ @thread = Thread.new do
103
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
104
+ end
105
+ @thread.abort_on_exception = true
106
+ @thread.report_on_exception = false
107
+ end
108
+
109
+ def wait_for_otp_code_expect_error_with_message(message)
110
+ @thread = Thread.new do
111
+ error = assert_raise Gem::WebauthnVerificationError do
112
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
113
+ end
114
+
115
+ assert_equal message, error.message
116
+ end
117
+ @thread.abort_on_exception = true
118
+ @thread.report_on_exception = false
119
+ end
120
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener/response"
5
+
6
+ class WebauthnListenerResponseTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @host = "rubygems.example"
10
+ end
11
+
12
+ def test_ok_response_to_s
13
+ to_s = Gem::WebauthnListener::OkResponse.new(@host).to_s
14
+
15
+ expected_to_s = <<~RESPONSE
16
+ HTTP/1.1 200 OK\r
17
+ connection: close\r
18
+ access-control-allow-origin: rubygems.example\r
19
+ access-control-allow-methods: POST\r
20
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
21
+ content-type: text/plain\r
22
+ content-length: 7\r
23
+ \r
24
+ success
25
+ RESPONSE
26
+
27
+ assert_equal expected_to_s, to_s
28
+ end
29
+
30
+ def test_no_to_s_response_to_s
31
+ to_s = Gem::WebauthnListener::NoContentResponse.new(@host).to_s
32
+
33
+ expected_to_s = <<~RESPONSE
34
+ HTTP/1.1 204 No Content\r
35
+ connection: close\r
36
+ access-control-allow-origin: rubygems.example\r
37
+ access-control-allow-methods: POST\r
38
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
39
+ \r
40
+ RESPONSE
41
+
42
+ assert_equal expected_to_s, to_s
43
+ end
44
+
45
+ def test_method_not_allowed_response_to_s
46
+ to_s = Gem::WebauthnListener::MethodNotAllowedResponse.new(@host).to_s
47
+
48
+ expected_to_s = <<~RESPONSE
49
+ HTTP/1.1 405 Method Not Allowed\r
50
+ connection: close\r
51
+ access-control-allow-origin: rubygems.example\r
52
+ access-control-allow-methods: POST\r
53
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
54
+ allow: GET, OPTIONS\r
55
+ \r
56
+ RESPONSE
57
+
58
+ assert_equal expected_to_s, to_s
59
+ end
60
+
61
+ def test_method_not_found_response_to_s
62
+ to_s = Gem::WebauthnListener::NotFoundResponse.new(@host).to_s
63
+
64
+ expected_to_s = <<~RESPONSE
65
+ HTTP/1.1 404 Not Found\r
66
+ connection: close\r
67
+ access-control-allow-origin: rubygems.example\r
68
+ access-control-allow-methods: POST\r
69
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
70
+ \r
71
+ RESPONSE
72
+
73
+ assert_equal expected_to_s, to_s
74
+ end
75
+
76
+ def test_bad_request_response_to_s
77
+ to_s = Gem::WebauthnListener::BadRequestResponse.new(@host).to_s
78
+
79
+ expected_to_s = <<~RESPONSE
80
+ HTTP/1.1 400 Bad Request\r
81
+ connection: close\r
82
+ access-control-allow-origin: rubygems.example\r
83
+ access-control-allow-methods: POST\r
84
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
85
+ content-type: text/plain\r
86
+ content-length: 22\r
87
+ \r
88
+ missing code parameter
89
+ RESPONSE
90
+
91
+ assert_equal expected_to_s, to_s
92
+ end
93
+ end
@@ -167,7 +167,7 @@ end
167
167
  #
168
168
  # Example:
169
169
  #
170
- # HTTPResponseFactory.create(
170
+ # Gem::HTTPResponseFactory.create(
171
171
  # body: "",
172
172
  # code: 301,
173
173
  # msg: "Moved Permanently",
@@ -175,7 +175,7 @@ end
175
175
  # )
176
176
  #
177
177
 
178
- class HTTPResponseFactory
178
+ class Gem::HTTPResponseFactory
179
179
  def self.create(body:, code:, msg:, headers: {})
180
180
  response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg)
181
181
  response.instance_variable_set(:@body, body)
@@ -186,6 +186,41 @@ class HTTPResponseFactory
186
186
  end
187
187
  end
188
188
 
189
+ ##
190
+ # A Gem::MockBrowser is used in tests to mock a browser in that it can
191
+ # send HTTP requests to the defined URI.
192
+ #
193
+ # Example:
194
+ #
195
+ # # Sends a get request to http://localhost:5678
196
+ # Gem::MockBrowser.get URI("http://localhost:5678")
197
+ #
198
+ # See RubyGems' tests for more examples of MockBrowser.
199
+ #
200
+
201
+ class Gem::MockBrowser
202
+ def self.options(uri)
203
+ options = Net::HTTP::Options.new(uri)
204
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
205
+ http.request(options)
206
+ end
207
+ end
208
+
209
+ def self.get(uri)
210
+ get = Net::HTTP::Get.new(uri)
211
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
212
+ http.request(get)
213
+ end
214
+ end
215
+
216
+ def self.post(uri)
217
+ post = Net::HTTP::Post.new(uri)
218
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
219
+ http.request(post)
220
+ end
221
+ end
222
+ end
223
+
189
224
  # :stopdoc:
190
225
  class Gem::RemoteFetcher
191
226
  def self.fetcher=(fetcher)
@@ -372,7 +407,7 @@ end
372
407
  #
373
408
  # This class was added to flush out problems in Rubinius' IO implementation.
374
409
 
375
- class TempIO < Tempfile
410
+ class Gem::TempIO < Tempfile
376
411
  ##
377
412
  # Creates a new TempIO that will be initialized to contain +string+.
378
413
 
@@ -391,3 +426,8 @@ class TempIO < Tempfile
391
426
  Gem.read_binary path
392
427
  end
393
428
  end
429
+
430
+ class Gem::TestCase
431
+ TempIO = Gem::TempIO
432
+ HTTPResponseFactory = Gem::HTTPResponseFactory
433
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubygems-update
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.10
4
+ version: 3.4.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Weirich
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2023-03-27 00:00:00.000000000 Z
19
+ date: 2023-04-11 00:00:00.000000000 Z
20
20
  dependencies: []
21
21
  description: |-
22
22
  A package (also known as a library) contains a set of functionality
@@ -596,6 +596,8 @@ files:
596
596
  - lib/rubygems/validator.rb
597
597
  - lib/rubygems/version.rb
598
598
  - lib/rubygems/version_option.rb
599
+ - lib/rubygems/webauthn_listener.rb
600
+ - lib/rubygems/webauthn_listener/response.rb
599
601
  - rubygems-update.gemspec
600
602
  - setup.rb
601
603
  - test/rubygems/alternate_cert.pem
@@ -809,6 +811,8 @@ files:
809
811
  - test/rubygems/test_remote_fetch_error.rb
810
812
  - test/rubygems/test_require.rb
811
813
  - test/rubygems/test_rubygems.rb
814
+ - test/rubygems/test_webauthn_listener.rb
815
+ - test/rubygems/test_webauthn_listener_response.rb
812
816
  - test/rubygems/utilities.rb
813
817
  - test/rubygems/wrong_key_cert.pem
814
818
  - test/rubygems/wrong_key_cert_32.pem
@@ -836,7 +840,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
836
840
  - !ruby/object:Gem::Version
837
841
  version: '0'
838
842
  requirements: []
839
- rubygems_version: 3.4.10
843
+ rubygems_version: 3.4.12
840
844
  signing_key:
841
845
  specification_version: 4
842
846
  summary: RubyGems is a package management framework for Ruby.