dependabot-bun 0.304.0 → 0.305.0
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 +4 -4
- data/lib/dependabot/bun/file_updater/bun_lockfile_updater.rb +1 -1
- data/lib/dependabot/bun/file_updater/npmrc_builder.rb +2 -2
- data/lib/dependabot/bun/metadata_finder.rb +2 -2
- data/lib/dependabot/bun/package/package_details_fetcher.rb +440 -0
- data/lib/dependabot/bun/package/registry_finder.rb +413 -0
- data/lib/dependabot/bun/update_checker/latest_version_finder.rb +532 -32
- data/lib/dependabot/bun/update_checker/library_detector.rb +1 -1
- data/lib/dependabot/bun/update_checker.rb +6 -3
- metadata +7 -6
- data/lib/dependabot/bun/update_checker/registry_finder.rb +0 -279
@@ -0,0 +1,413 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "excon"
|
5
|
+
require "dependabot/bun/update_checker"
|
6
|
+
require "dependabot/registry_client"
|
7
|
+
require "sorbet-runtime"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Bun
|
11
|
+
module Package
|
12
|
+
class RegistryFinder
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
GLOBAL_NPM_REGISTRY = "https://registry.npmjs.org"
|
16
|
+
|
17
|
+
CENTRAL_REGISTRIES = %w(
|
18
|
+
https://registry.npmjs.org
|
19
|
+
http://registry.npmjs.org
|
20
|
+
https://registry.yarnpkg.com
|
21
|
+
http://registry.yarnpkg.com
|
22
|
+
).freeze
|
23
|
+
NPM_AUTH_TOKEN_REGEX = %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}
|
24
|
+
NPM_GLOBAL_REGISTRY_REGEX = /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
25
|
+
YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+((['"](?<registry>.*)['"])|(?<registry>.*))/
|
26
|
+
NPM_SCOPED_REGISTRY_REGEX = /^(?<scope>@[^:]+)\s*:registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
27
|
+
YARN_SCOPED_REGISTRY_REGEX = /['"](?<scope>@[^:]+):registry['"]\s((['"](?<registry>.*)['"])|(?<registry>.*))/
|
28
|
+
|
29
|
+
sig do
|
30
|
+
params(
|
31
|
+
dependency: T.nilable(Dependabot::Dependency),
|
32
|
+
credentials: T::Array[Dependabot::Credential],
|
33
|
+
npmrc_file: T.nilable(Dependabot::DependencyFile),
|
34
|
+
yarnrc_file: T.nilable(Dependabot::DependencyFile),
|
35
|
+
yarnrc_yml_file: T.nilable(Dependabot::DependencyFile)
|
36
|
+
).void
|
37
|
+
end
|
38
|
+
def initialize(dependency:, credentials:, npmrc_file: nil,
|
39
|
+
yarnrc_file: nil, yarnrc_yml_file: nil)
|
40
|
+
@dependency = dependency
|
41
|
+
@credentials = credentials
|
42
|
+
@npmrc_file = npmrc_file
|
43
|
+
@yarnrc_file = yarnrc_file
|
44
|
+
@yarnrc_yml_file = yarnrc_yml_file
|
45
|
+
|
46
|
+
@registry = T.let(nil, T.nilable(String))
|
47
|
+
@first_registry_with_dependency_details = T.let(nil, T.nilable(String))
|
48
|
+
@known_registries = T.let([], T::Array[T::Hash[String, T.nilable(String)]])
|
49
|
+
@configured_global_registry = T.let(nil, T.nilable(String))
|
50
|
+
@global_registry = T.let(nil, T.nilable(String))
|
51
|
+
@parsed_yarnrc_yml = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { returns(String) }
|
55
|
+
def registry
|
56
|
+
return @registry if @registry
|
57
|
+
|
58
|
+
@registry = locked_registry || configured_registry || first_registry_with_dependency_details
|
59
|
+
T.must(@registry)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { returns(T::Hash[String, String]) }
|
63
|
+
def auth_headers
|
64
|
+
auth_header_for(auth_token)
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { returns(String) }
|
68
|
+
def dependency_url
|
69
|
+
"#{registry_url}/#{escaped_dependency_name}"
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(version: T.any(String, Gem::Version)).returns(String) }
|
73
|
+
def tarball_url(version)
|
74
|
+
version_without_build_metadata = version.to_s.gsub(/\+.*/, "")
|
75
|
+
|
76
|
+
# Dependency name needs to be unescaped since tarball URLs don't always work with escaped slashes
|
77
|
+
"#{registry_url}/#{dependency&.name}/-/#{scopeless_name}-#{version_without_build_metadata}.tgz"
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { params(registry: String).returns(T::Boolean) }
|
81
|
+
def self.central_registry?(registry)
|
82
|
+
CENTRAL_REGISTRIES.any? do |r|
|
83
|
+
r.include?(registry)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
88
|
+
def registry_from_rc(dependency_name)
|
89
|
+
explicit_registry_from_rc(dependency_name) || global_registry
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { returns(T::Boolean) }
|
93
|
+
def custom_registry?
|
94
|
+
return false if CENTRAL_REGISTRIES.include?(registry_url)
|
95
|
+
|
96
|
+
!(registry_url || "").match?(/registry\.npmjs\.(org|com)/)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
sig { returns(T.nilable(Dependabot::Dependency)) }
|
102
|
+
attr_reader :dependency
|
103
|
+
|
104
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
105
|
+
attr_reader :credentials
|
106
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
107
|
+
attr_reader :npmrc_file
|
108
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
109
|
+
attr_reader :yarnrc_file
|
110
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
111
|
+
attr_reader :yarnrc_yml_file
|
112
|
+
|
113
|
+
sig { params(dependency_name: T.nilable(String)).returns(T.nilable(String)) }
|
114
|
+
def explicit_registry_from_rc(dependency_name)
|
115
|
+
if dependency_name&.start_with?("@") && dependency_name.include?("/")
|
116
|
+
scope = dependency_name.split("/").first
|
117
|
+
scoped_registry(T.must(scope)) || configured_global_registry
|
118
|
+
else
|
119
|
+
configured_global_registry
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
sig { returns(T.nilable(String)) }
|
124
|
+
def first_registry_with_dependency_details
|
125
|
+
return @first_registry_with_dependency_details if @first_registry_with_dependency_details
|
126
|
+
|
127
|
+
@first_registry_with_dependency_details ||=
|
128
|
+
known_registries.find do |details|
|
129
|
+
url = "#{details['registry']&.gsub(%r{/+$}, '')}/#{escaped_dependency_name}"
|
130
|
+
url = "https://#{url}" unless url.start_with?("http")
|
131
|
+
response = Dependabot::RegistryClient.get(
|
132
|
+
url: url,
|
133
|
+
headers: auth_header_for(details["token"])
|
134
|
+
)
|
135
|
+
response.status < 400 && JSON.parse(response.body)
|
136
|
+
rescue Excon::Error::Timeout,
|
137
|
+
Excon::Error::Socket,
|
138
|
+
JSON::ParserError
|
139
|
+
nil
|
140
|
+
rescue URI::InvalidURIError => e
|
141
|
+
raise DependencyFileNotResolvable, e.message
|
142
|
+
end&.fetch("registry")
|
143
|
+
|
144
|
+
@first_registry_with_dependency_details ||= global_registry.sub(%r{/+$}, "").sub(%r{^.*?//}, "")
|
145
|
+
end
|
146
|
+
|
147
|
+
sig { returns(T.nilable(String)) }
|
148
|
+
def registry_url
|
149
|
+
url =
|
150
|
+
if registry.start_with?("http")
|
151
|
+
registry
|
152
|
+
else
|
153
|
+
protocol =
|
154
|
+
if registry_source_url
|
155
|
+
registry_source_url&.split("://")&.first
|
156
|
+
else
|
157
|
+
"https"
|
158
|
+
end
|
159
|
+
|
160
|
+
"#{protocol}://#{registry}"
|
161
|
+
end
|
162
|
+
|
163
|
+
url.gsub(%r{/+$}, "")
|
164
|
+
end
|
165
|
+
|
166
|
+
sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) }
|
167
|
+
def auth_header_for(token)
|
168
|
+
return {} unless token
|
169
|
+
|
170
|
+
if token.include?(":")
|
171
|
+
encoded_token = Base64.encode64(token).delete("\n")
|
172
|
+
{ "Authorization" => "Basic #{encoded_token}" }
|
173
|
+
elsif Base64.decode64(token).ascii_only? &&
|
174
|
+
Base64.decode64(token).include?(":")
|
175
|
+
{ "Authorization" => "Basic #{token.delete("\n")}" }
|
176
|
+
else
|
177
|
+
{ "Authorization" => "Bearer #{token}" }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
sig { returns(T.nilable(String)) }
|
182
|
+
def auth_token
|
183
|
+
known_registries
|
184
|
+
.find { |cred| cred["registry"] == registry }
|
185
|
+
&.fetch("token", nil)
|
186
|
+
end
|
187
|
+
|
188
|
+
sig { returns(T.nilable(String)) }
|
189
|
+
def locked_registry
|
190
|
+
return unless registry_source_url
|
191
|
+
|
192
|
+
lockfile_registry =
|
193
|
+
registry_source_url
|
194
|
+
&.gsub("https://", "")
|
195
|
+
&.gsub("http://", "")
|
196
|
+
|
197
|
+
if lockfile_registry
|
198
|
+
detailed_registry =
|
199
|
+
known_registries
|
200
|
+
.find { |h| h["registry"]&.include?(lockfile_registry) }
|
201
|
+
&.fetch("registry")
|
202
|
+
end
|
203
|
+
|
204
|
+
detailed_registry || lockfile_registry
|
205
|
+
end
|
206
|
+
|
207
|
+
sig { returns(T.nilable(String)) }
|
208
|
+
def configured_registry
|
209
|
+
configured_registry_url = explicit_registry_from_rc(dependency&.name)
|
210
|
+
return unless configured_registry_url
|
211
|
+
|
212
|
+
normalize_configured_registry(configured_registry_url)
|
213
|
+
end
|
214
|
+
|
215
|
+
sig { returns(T::Array[T::Hash[String, T.nilable(String)]]) }
|
216
|
+
def known_registries
|
217
|
+
return @known_registries if @known_registries.any?
|
218
|
+
|
219
|
+
@known_registries =
|
220
|
+
begin
|
221
|
+
registries = []
|
222
|
+
registries += credentials
|
223
|
+
.select { |cred| cred["type"] == "npm_registry" && cred["registry"] }
|
224
|
+
.tap { |arr| arr.each { |c| c["token"] ||= nil } }
|
225
|
+
registries += npmrc_registries
|
226
|
+
registries += yarnrc_registries
|
227
|
+
|
228
|
+
unique_registries(registries)
|
229
|
+
end
|
230
|
+
@known_registries
|
231
|
+
end
|
232
|
+
|
233
|
+
sig { returns(T::Array[T::Hash[String, T.nilable(String)]]) }
|
234
|
+
def npmrc_registries
|
235
|
+
return [] unless npmrc_file
|
236
|
+
|
237
|
+
registries = []
|
238
|
+
npmrc_file&.content&.scan(NPM_AUTH_TOKEN_REGEX) do
|
239
|
+
next if Regexp.last_match&.[](:registry)&.include?("${")
|
240
|
+
|
241
|
+
registry = T.must(Regexp.last_match)[:registry]
|
242
|
+
token = T.must(Regexp.last_match)[:token]&.strip
|
243
|
+
|
244
|
+
registries << {
|
245
|
+
"type" => "npm_registry",
|
246
|
+
"registry" => registry&.gsub(/\s+/, "%20"),
|
247
|
+
"token" => token
|
248
|
+
}
|
249
|
+
end
|
250
|
+
|
251
|
+
registries += npmrc_global_registries
|
252
|
+
end
|
253
|
+
|
254
|
+
sig { returns(T::Array[T::Hash[String, T.nilable(String)]]) }
|
255
|
+
def yarnrc_registries
|
256
|
+
return [] unless yarnrc_file
|
257
|
+
|
258
|
+
yarnrc_global_registries
|
259
|
+
end
|
260
|
+
|
261
|
+
sig do
|
262
|
+
params(registries: T::Array[T::Hash[String, T.nilable(String)]])
|
263
|
+
.returns(T::Array[T::Hash[String, T.nilable(String)]])
|
264
|
+
end
|
265
|
+
def unique_registries(registries)
|
266
|
+
registries.uniq.reject do |registry|
|
267
|
+
next if registry["token"]
|
268
|
+
|
269
|
+
# Reject this entry if an identical one with a token exists
|
270
|
+
registries.any? do |r|
|
271
|
+
r["token"] && r["registry"] == registry["registry"]
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
sig { returns(String) }
|
277
|
+
def global_registry
|
278
|
+
return @global_registry if @global_registry
|
279
|
+
|
280
|
+
@global_registry = configured_global_registry || GLOBAL_NPM_REGISTRY
|
281
|
+
@global_registry
|
282
|
+
end
|
283
|
+
|
284
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
285
|
+
sig { returns(T.nilable(String)) }
|
286
|
+
def configured_global_registry
|
287
|
+
return @configured_global_registry if @configured_global_registry
|
288
|
+
|
289
|
+
@configured_global_registry = (npmrc_file && npmrc_global_registries.first&.fetch("url")) ||
|
290
|
+
(yarnrc_file && yarnrc_global_registries.first&.fetch("url"))
|
291
|
+
return @configured_global_registry if @configured_global_registry
|
292
|
+
|
293
|
+
if parsed_yarnrc_yml&.key?("npmRegistryServer")
|
294
|
+
return @configured_global_registry = T.must(parsed_yarnrc_yml)["npmRegistryServer"]
|
295
|
+
end
|
296
|
+
|
297
|
+
replaces_base = credentials.find { |cred| cred["type"] == "npm_registry" && cred.replaces_base? }
|
298
|
+
if replaces_base
|
299
|
+
registry = replaces_base["registry"]
|
300
|
+
registry = "https://#{registry}" unless registry&.start_with?("http")
|
301
|
+
return @configured_global_registry = registry
|
302
|
+
end
|
303
|
+
|
304
|
+
@configured_global_registry = nil
|
305
|
+
end
|
306
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
307
|
+
|
308
|
+
sig { returns(T::Array[T::Hash[String, String]]) }
|
309
|
+
def npmrc_global_registries
|
310
|
+
global_rc_registries(npmrc_file, syntax: NPM_GLOBAL_REGISTRY_REGEX)
|
311
|
+
end
|
312
|
+
|
313
|
+
sig { returns(T::Array[T::Hash[String, String]]) }
|
314
|
+
def yarnrc_global_registries
|
315
|
+
global_rc_registries(yarnrc_file, syntax: YARN_GLOBAL_REGISTRY_REGEX)
|
316
|
+
end
|
317
|
+
|
318
|
+
sig { params(scope: String).returns(T.nilable(String)) }
|
319
|
+
def scoped_registry(scope)
|
320
|
+
scoped_rc_registry = scoped_rc_registry(npmrc_file, syntax: NPM_SCOPED_REGISTRY_REGEX, scope: scope) ||
|
321
|
+
scoped_rc_registry(yarnrc_file, syntax: YARN_SCOPED_REGISTRY_REGEX, scope: scope)
|
322
|
+
return scoped_rc_registry if scoped_rc_registry
|
323
|
+
|
324
|
+
if parsed_yarnrc_yml
|
325
|
+
yarn_berry_registry = parsed_yarnrc_yml&.dig("npmScopes", scope.delete_prefix("@"), "npmRegistryServer")
|
326
|
+
return yarn_berry_registry if yarn_berry_registry
|
327
|
+
end
|
328
|
+
|
329
|
+
nil
|
330
|
+
end
|
331
|
+
|
332
|
+
sig do
|
333
|
+
params(
|
334
|
+
file: T.nilable(Dependabot::DependencyFile),
|
335
|
+
syntax: T.any(String, Regexp)
|
336
|
+
).returns(T::Array[T::Hash[String, String]])
|
337
|
+
end
|
338
|
+
def global_rc_registries(file, syntax:)
|
339
|
+
registries = []
|
340
|
+
|
341
|
+
file&.content&.scan(syntax) do
|
342
|
+
next if Regexp.last_match&.[](:registry)&.include?("${")
|
343
|
+
|
344
|
+
url = T.must(T.must(Regexp.last_match)[:registry]).strip
|
345
|
+
registry = normalize_configured_registry(url)
|
346
|
+
registries << {
|
347
|
+
"type" => "npm_registry",
|
348
|
+
"registry" => registry,
|
349
|
+
"url" => url,
|
350
|
+
"token" => nil
|
351
|
+
}
|
352
|
+
end
|
353
|
+
|
354
|
+
registries
|
355
|
+
end
|
356
|
+
|
357
|
+
sig do
|
358
|
+
params(
|
359
|
+
file: T.nilable(Dependabot::DependencyFile),
|
360
|
+
syntax: T.any(String, Regexp),
|
361
|
+
scope: String
|
362
|
+
).returns(T.nilable(String))
|
363
|
+
end
|
364
|
+
def scoped_rc_registry(file, syntax:, scope:)
|
365
|
+
file&.content.to_s.scan(syntax) do
|
366
|
+
next if Regexp.last_match&.[](:registry)&.include?("${") || Regexp.last_match&.[](:scope) != scope
|
367
|
+
|
368
|
+
return T.must(T.must(Regexp.last_match)[:registry]).strip
|
369
|
+
end
|
370
|
+
|
371
|
+
nil
|
372
|
+
end
|
373
|
+
|
374
|
+
# npm registries expect slashes to be escaped
|
375
|
+
sig { returns(T.nilable(String)) }
|
376
|
+
def escaped_dependency_name
|
377
|
+
dependency&.name&.gsub("/", "%2F")
|
378
|
+
end
|
379
|
+
|
380
|
+
sig { returns(T.nilable(String)) }
|
381
|
+
def scopeless_name
|
382
|
+
dependency&.name&.split("/")&.last
|
383
|
+
end
|
384
|
+
|
385
|
+
sig { returns(T.nilable(String)) }
|
386
|
+
def registry_source_url # rubocop:disable Metrics/PerceivedComplexity
|
387
|
+
sources = dependency&.requirements
|
388
|
+
&.map { |r| r.fetch(:source) }&.uniq&.compact
|
389
|
+
&.sort_by { |source| self.class.central_registry?(source[:url]) ? 1 : 0 }
|
390
|
+
|
391
|
+
sources&.find { |s| s[:type] == "registry" }&.fetch(:url)
|
392
|
+
end
|
393
|
+
|
394
|
+
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
|
395
|
+
def parsed_yarnrc_yml
|
396
|
+
yarnrc_yml_file_content = yarnrc_yml_file&.content
|
397
|
+
return unless yarnrc_yml_file_content
|
398
|
+
return @parsed_yarnrc_yml if @parsed_yarnrc_yml
|
399
|
+
|
400
|
+
@parsed_yarnrc_yml = YAML.safe_load(yarnrc_yml_file_content)
|
401
|
+
@parsed_yarnrc_yml
|
402
|
+
end
|
403
|
+
|
404
|
+
sig { params(url: String).returns(String) }
|
405
|
+
def normalize_configured_registry(url)
|
406
|
+
url.sub(%r{/+$}, "")
|
407
|
+
.sub(%r{^.*?//}, "")
|
408
|
+
.gsub(/\s+/, "%20")
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|