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.
@@ -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