pray-cli 1.9.0 → 1.9.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: 2a25d60eee0adb8e74f902347fc6d429d489803628d96abc8f73c154be9c993d
4
- data.tar.gz: 318dcf2ddeeecc69231562f20f6146368c4e68758726e153a7556f2e4a925333
3
+ metadata.gz: 85c1a7462b69a935d6e9a83d1489c43a445e63190dcebb5481744de051bf562a
4
+ data.tar.gz: 778142b6ea1ac16ae0762dc9015a30f20ca06cfb3abcb715fa37eea2620a05ff
5
5
  SHA512:
6
- metadata.gz: a2fa874e166e17456197f6db47d35f64d153867900f1837fd69b5802a6c910a2a9ae3983f035df8b2e0959a0ba9163c6caea9314cd56161a6b6d18fd8fc63448
7
- data.tar.gz: 60a8103a9a74e25729619107bfa80e8bbea1e4d85a554bd428e0280ec53600d978bf64afcceac84be37ceaaea7f6a7628dc9efd234e1c29b7388b0d2b3935bf0
6
+ metadata.gz: 7199ad20e75fd83ad98c97d0bbf9f97ae4c6d91bc2e2a8dca0cf4b14339da30a7396d8b076e0f789d8f17f0334b2b10853c8298e429705ef99ccaccc02b88f09
7
+ data.tar.gz: 19ea0fb475fdfeabc7ab7729e40d2228eba70be9f52becd2923d9b2c770a31fc146215aa2fa65b04c2e14733114c672f67a906e364841df027fedbcd9ca6f61f
data/CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Unreleased
3
+ ## 1.9.2 (2026-09-01)
4
+
5
+ - Fix `.praypkg` unpack for git and registry installs when the Ruby process forces UTF-8 internal encoding.
6
+ - Treat empty or corrupt `.pray/cache/registry` directories as not ready so the next install unpacks again.
7
+ - Reject archive members that escape the package root or exceed size limits; unpack through staging into cache.
8
+ - Resolve packages from a matching source namespace or sole source without an explicit `source:`.
9
+ - Move login sessions to the user Pray home with owner-only permissions and migrate legacy repository sessions.
10
+ - Reject manifest paths outside the project; require registry hashes and recheck cached package trees.
11
+ - Bound server requests, headers, connections, and timeouts; expose a readiness endpoint.
12
+ - Cap registry downloads at 64 MiB; unpack `.praypkg` tar members without system tar; reject absolute artifact URLs.
13
+
14
+ ## 1.9.1 (2026-08-29)
15
+
16
+ - Rewrite every matching `pray` line when a package constraint is updated, keeping indent and extra keywords.
4
17
 
5
18
  ## 1.7.0 (2026-07-29)
6
19
 
data/bin/pray CHANGED
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  Encoding.default_external = Encoding::UTF_8
5
- Encoding.default_internal = Encoding::UTF_8
6
5
 
7
6
  gemfile = File.expand_path("../Gemfile", __dir__)
8
7
  if File.exist?(gemfile)
data/lib/pray/archive.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
  require "open3"
5
5
  require "fileutils"
6
- require "tempfile"
6
+ require_relative "archive_unpack"
7
7
 
8
8
  module Pray
9
9
  module Archive
@@ -23,15 +23,20 @@ module Pray
23
23
  File.binwrite(destination, File.binread(File.join(package.root, file)))
24
24
  end
25
25
 
26
- tar_bytes, status = Open3.capture2("tar", "-cf", "-", "-C", staging, ".")
27
- raise Error.integrity("failed to build package tar archive") unless status.success?
26
+ with_binary_process_encoding do
27
+ tar_bytes, status = Open3.capture2(
28
+ {"COPYFILE_DISABLE" => "1"},
29
+ "tar", "-cf", "-", "-C", staging, "."
30
+ )
31
+ raise Error.integrity("failed to build package tar archive") unless status.success?
28
32
 
29
- zstd_bytes, status = Open3.capture2("zstd", "-q", "-c", stdin_data: tar_bytes)
30
- unless status.success?
31
- raise Error.unsupported("zstd is required to build package archives")
32
- end
33
+ zstd_bytes, status = Open3.capture2("zstd", "-q", "-c", stdin_data: tar_bytes)
34
+ unless status.success?
35
+ raise Error.unsupported("zstd is required to build package archives")
36
+ end
33
37
 
34
- zstd_bytes
38
+ zstd_bytes
39
+ end
35
40
  end
36
41
  end
37
42
 
@@ -41,14 +46,7 @@ module Pray
41
46
  end
42
47
 
43
48
  def unpack_praypkg(artifact_bytes, output_directory)
44
- FileUtils.mkdir_p(output_directory)
45
- tar_bytes, status = Open3.capture2("zstd", "-d", "-q", "-c", stdin_data: artifact_bytes)
46
- unless status.success?
47
- raise Error.unsupported("zstd is required to unpack package archives")
48
- end
49
-
50
- _stdout, _stderr, status = Open3.capture3("tar", "-xf", "-", "-C", output_directory, stdin_data: tar_bytes)
51
- raise Error.integrity("failed to unpack package archive") unless status.success?
49
+ ArchiveUnpack.unpack_praypkg(artifact_bytes, output_directory)
52
50
  end
53
51
 
54
52
  def package_archive_path(package_name, version)
@@ -64,5 +62,9 @@ module Pray
64
62
  "exports" => package.selected_exports
65
63
  )
66
64
  end
65
+
66
+ def with_binary_process_encoding(&block)
67
+ ArchiveUnpack.with_binary_process_encoding(&block)
68
+ end
67
69
  end
68
70
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+ require "find"
6
+ require_relative "path_safety"
7
+ require_relative "resource_limits"
8
+ require_relative "tar_validation"
9
+
10
+ module Pray
11
+ module ArchiveUnpack
12
+ module_function
13
+
14
+ def unpack_praypkg(artifact_bytes, output_directory)
15
+ if artifact_bytes.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
16
+ raise Error.integrity(
17
+ "package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes"
18
+ )
19
+ end
20
+
21
+ tar_bytes = decompress_zstd(artifact_bytes)
22
+ if tar_bytes.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
23
+ raise Error.integrity(
24
+ "package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} decompressed bytes"
25
+ )
26
+ end
27
+
28
+ TarValidation.extract!(tar_bytes, output_directory)
29
+ reject_unsafe_extracted_tree!(output_directory)
30
+ end
31
+
32
+ def with_binary_process_encoding
33
+ previous = Encoding.default_internal
34
+ verbose = $VERBOSE
35
+ $VERBOSE = nil
36
+ Encoding.default_internal = nil
37
+ yield
38
+ ensure
39
+ Encoding.default_internal = previous
40
+ $VERBOSE = verbose
41
+ end
42
+
43
+ def decompress_zstd(artifact_bytes)
44
+ with_binary_process_encoding do
45
+ tar_bytes, status = Open3.capture2("zstd", "-d", "-q", "-c", stdin_data: artifact_bytes)
46
+ unless status.success?
47
+ raise Error.unsupported("zstd is required to unpack package archives")
48
+ end
49
+
50
+ tar_bytes
51
+ end
52
+ end
53
+
54
+ def reject_unsafe_extracted_tree!(output_directory)
55
+ Find.find(output_directory) do |path|
56
+ next if path == output_directory
57
+ next if File.directory?(path)
58
+
59
+ if File.symlink?(path)
60
+ raise Error.integrity("unsupported package archive entry type")
61
+ end
62
+ unless PathSafety.path_under_root?(output_directory, path)
63
+ raise Error.integrity("package path escapes package root: #{path}")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource_limits"
4
+
5
+ module Pray
6
+ module HttpBody
7
+ module_function
8
+
9
+ def reject_oversized_content_length!(content_length, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
10
+ return if content_length.nil?
11
+ return if content_length <= max_bytes
12
+
13
+ raise Error.resolution("HTTP response exceeds #{max_bytes} bytes")
14
+ end
15
+
16
+ def append_chunk!(body, chunk, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
17
+ body << chunk
18
+ return body if body.bytesize <= max_bytes
19
+
20
+ raise Error.resolution("HTTP response exceeds #{max_bytes} bytes")
21
+ end
22
+
23
+ def read_response!(response, max_bytes = ResourceLimits::MAX_HTTP_RESPONSE_BYTES)
24
+ length = response["content-length"]
25
+ parsed = length && Integer(length, exception: false)
26
+ reject_oversized_content_length!(parsed, max_bytes)
27
+ body = +"".b
28
+ response.read_body do |chunk|
29
+ append_chunk!(body, chunk, max_bytes)
30
+ end
31
+ body
32
+ end
33
+ end
34
+ end
data/lib/pray/manifest.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "manifest_formatter"
5
5
  require_relative "manifest_parser_helpers"
6
6
  require_relative "manifest_parser_blocks"
7
7
  require_relative "manifest_parser"
8
+ require_relative "path_safety"
8
9
 
9
10
  module Pray
10
11
  RenderPolicy = Struct.new(
@@ -118,7 +119,22 @@ module Pray
118
119
 
119
120
  def parse_manifest(text)
120
121
  lines = Literal.prepare_parser_lines(text)
121
- BlockParser.new(lines).parse_root
122
+ manifest = BlockParser.new(lines).parse_root
123
+ validate_manifest_paths!(manifest)
124
+ manifest
125
+ end
126
+
127
+ def validate_manifest_paths!(manifest)
128
+ manifest.targets.each do |target|
129
+ (target.outputs + target.skills + target.commands + target.rules).each do |path|
130
+ PathSafety.validate_project_relative_path!(path)
131
+ end
132
+ end
133
+ manifest.packages.each do |package|
134
+ PathSafety.validate_project_relative_path!(package.path) if package.path
135
+ PathSafety.validate_project_relative_path!(package.file) if package.file
136
+ end
137
+ manifest.local.each { |local| PathSafety.validate_project_relative_path!(local.path) }
122
138
  end
123
139
  end
124
140
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ManifestMethods
5
+ module_function
6
+
7
+ PACKAGE_KEYWORDS = %w[pray use include agent package].freeze
8
+
9
+ def rewrite_constraint_on_line(line, constraint)
10
+ indent = line[/\A\s*/]
11
+ trimmed = line.lstrip
12
+ after_keyword = skip_package_keyword(trimmed)
13
+ raise Error.manifest("package declaration is missing a keyword") unless after_keyword
14
+
15
+ after_keyword = after_keyword.lstrip
16
+ parsed_name = parse_quoted(after_keyword)
17
+ raise Error.manifest("package declaration is missing a quoted name") unless parsed_name
18
+
19
+ name, after_name = parsed_name
20
+ quoted_constraint = "\"#{constraint}\""
21
+ keyword_and_name = trimmed[0, trimmed.length - after_name.length]
22
+ remainder = after_name.lstrip
23
+ return "#{indent}#{keyword_and_name}, #{quoted_constraint}" if remainder.empty?
24
+ unless remainder.start_with?(",")
25
+ raise Error.manifest("package #{name} declaration is missing a comma after the name")
26
+ end
27
+
28
+ after_comma = remainder[1..].lstrip
29
+ if after_comma.start_with?("\"", "'")
30
+ parsed_constraint = parse_quoted(after_comma)
31
+ unless parsed_constraint
32
+ raise Error.manifest("package #{name} declaration has an unclosed constraint")
33
+ end
34
+
35
+ return "#{indent}#{keyword_and_name}, #{quoted_constraint}#{parsed_constraint[1]}"
36
+ end
37
+
38
+ "#{indent}#{keyword_and_name}, #{quoted_constraint}, #{after_comma}"
39
+ end
40
+
41
+ def skip_package_keyword(input)
42
+ PACKAGE_KEYWORDS.each do |keyword|
43
+ next unless input.start_with?(keyword)
44
+
45
+ rest = input[keyword.length..]
46
+ next_character = rest[0]
47
+ whitespace_or_quote = next_character.nil? ||
48
+ next_character.match?(/\s/) ||
49
+ next_character == "\"" ||
50
+ next_character == "'"
51
+ return rest if whitespace_or_quote
52
+ end
53
+ nil
54
+ end
55
+
56
+ def parse_quoted(input)
57
+ quote = input[0]
58
+ return unless quote == "\"" || quote == "'"
59
+
60
+ rest = input[1..]
61
+ ending = rest.index(quote)
62
+ return unless ending
63
+
64
+ [rest[0, ending], rest[(ending + 1)..]]
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "manifest_constraint"
4
+
3
5
  module Pray
4
6
  module ManifestMethods
5
7
  module_function
@@ -37,13 +39,15 @@ module Pray
37
39
  "package \"#{name}\"", "package '#{name}'"
38
40
  ]
39
41
  lines = text.lines.map(&:chomp)
40
- index = lines.index { |line|
41
- trimmed = line.lstrip
42
- prefixes.any? { |prefix| trimmed.start_with?(prefix) }
43
- }
44
- raise Error.manifest("package #{name} not found in manifest") unless index
42
+ replaced = 0
43
+ lines.each_index do |index|
44
+ trimmed = lines[index].lstrip
45
+ next unless prefixes.any? { |prefix| trimmed.start_with?(prefix) }
45
46
 
46
- lines[index] = format_package_declaration(package)
47
+ lines[index] = rewrite_constraint_on_line(lines[index], package.constraint)
48
+ replaced += 1
49
+ end
50
+ raise Error.manifest("package #{name} not found in manifest") if replaced.zero?
47
51
  output = lines.join("\n")
48
52
  output += "\n" if text.end_with?("\n") && !output.end_with?("\n")
49
53
  output
@@ -37,5 +37,39 @@ module Pray
37
37
 
38
38
  cleaned
39
39
  end
40
+
41
+ def validate_archive_member_path!(path)
42
+ cleaned = path.to_s.delete_prefix("./")
43
+ if cleaned.empty? || Pathname.new(cleaned).absolute? || cleaned.start_with?("/")
44
+ raise Error.integrity("package path must be relative: #{path}")
45
+ end
46
+
47
+ cleaned.split("/").each do |part|
48
+ next if part.empty? || part == "."
49
+
50
+ if part == ".." || part.include?("\0")
51
+ raise Error.integrity("package path escapes package root: #{path}")
52
+ end
53
+ end
54
+
55
+ cleaned
56
+ end
57
+
58
+ def validate_project_relative_path!(value)
59
+ path = value.to_s.strip
60
+ windows_absolute = path.match?(/\A(?:[A-Za-z]:[\\\/]|[\\\/]{2})/)
61
+ if path.empty? || Pathname.new(path).absolute? || windows_absolute
62
+ raise Error.manifest("project path must be repository-relative: #{value}")
63
+ end
64
+ parts = path.tr("\\", "/").split("/")
65
+ if parts.include?("..") || path.include?("\0")
66
+ raise Error.manifest("project path escapes repository root: #{value}")
67
+ end
68
+ if parts.all? { |part| part.empty? || part == "." }
69
+ raise Error.manifest("project path must be repository-relative: #{value}")
70
+ end
71
+
72
+ path
73
+ end
40
74
  end
41
75
  end
data/lib/pray/registry.rb CHANGED
@@ -6,6 +6,7 @@ require "uri"
6
6
  require "fileutils"
7
7
  require "pathname"
8
8
  require_relative "path_safety"
9
+ require_relative "http_body"
9
10
 
10
11
  module Pray
11
12
  RegistryPackageVersion = Struct.new(
@@ -33,12 +34,15 @@ module Pray
33
34
  )
34
35
 
35
36
  module Registry
37
+ extend RegistryIntegrity
38
+
36
39
  module_function
37
40
 
38
41
  def resolve_registry_package_root(project_root, source_url, declaration, preferred_version: nil, offline: false)
39
42
  metadata = fetch_package_metadata(source_url, declaration.name)
40
43
  registry_latest_version = registry_latest_version_label(metadata)
41
44
  selected = select_package_version(metadata, declaration.constraint, preferred_version)
45
+ require_integrity_fields!(declaration.name, selected)
42
46
  cache_directory = registry_cache_directory(
43
47
  project_root,
44
48
  source_url,
@@ -57,11 +61,10 @@ module Pray
57
61
 
58
62
  raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
59
63
 
60
- FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
61
- FileUtils.mkdir_p(cache_directory)
62
-
63
64
  artifact_bytes = read_artifact_bytes(source_url, selected.artifact)
64
- validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: source_url)
65
+ RegistryInstall.install_artifact_to_cache(
66
+ cache_directory, declaration, selected, artifact_bytes, source_url: source_url
67
+ )
65
68
 
66
69
  RegistryPackageResolution.new(
67
70
  root: cache_directory,
@@ -83,6 +86,7 @@ module Pray
83
86
  metadata = parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
84
87
  registry_latest_version = registry_latest_version_label(metadata)
85
88
  selected = select_package_version(metadata, declaration.constraint, preferred_version)
89
+ require_integrity_fields!(declaration.name, selected)
86
90
  cache_directory = registry_cache_directory(
87
91
  project_root,
88
92
  source_key,
@@ -101,11 +105,10 @@ module Pray
101
105
 
102
106
  raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
103
107
 
104
- FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
105
- FileUtils.mkdir_p(cache_directory)
106
-
107
108
  artifact_bytes = read_local_artifact_bytes(source_root, selected.artifact)
108
- validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: source_root)
109
+ RegistryInstall.install_artifact_to_cache(
110
+ cache_directory, declaration, selected, artifact_bytes, source_url: source_root
111
+ )
109
112
 
110
113
  RegistryPackageResolution.new(
111
114
  root: cache_directory,
@@ -177,51 +180,13 @@ module Pray
177
180
  Gem::Version.new(left) <=> Gem::Version.new(right)
178
181
  end
179
182
 
180
- def validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
181
- if selected.artifact_hash
182
- artifact_hash = Hashing.sha256_prefixed(artifact_bytes)
183
- if artifact_hash != selected.artifact_hash
184
- raise Error.integrity(
185
- "package artifact hash mismatch for #{declaration.name} #{selected.version}"
186
- )
187
- end
188
- end
189
-
190
- verify_registry_signature!(declaration, selected, artifact_bytes)
191
- Trust.verify_publisher_fingerprint!(source_url, selected) if source_url
192
-
193
- Archive.unpack_praypkg(artifact_bytes, cache_directory)
194
- spec_path = Resolve.find_prayspec_file(cache_directory)
195
- spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
196
- if spec.name != declaration.name
197
- raise Error.resolution(
198
- "package path #{cache_directory.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
199
- )
200
- end
201
- if spec.version != selected.version
202
- raise Error.resolution(
203
- "package #{declaration.name} version #{spec.version} does not match registry version #{selected.version}"
204
- )
205
- end
206
- if selected.tree_hash
207
- actual_tree_hash = spec.tree_hash_for_root(cache_directory)
208
- if actual_tree_hash != selected.tree_hash
209
- raise Error.integrity(
210
- "package tree hash mismatch for #{declaration.name} #{selected.version}"
211
- )
212
- end
213
- end
214
- end
215
-
216
183
  def read_local_artifact_bytes(source_root, artifact)
217
- if artifact.start_with?("http://", "https://")
218
- return http_get(artifact)
219
- end
220
184
  if artifact.start_with?("file://")
221
185
  path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
222
186
  raise Error.resolution("package artifact path escapes distribution root") unless path
223
187
  return File.binread(path)
224
188
  end
189
+ reject_absolute_artifact!(artifact)
225
190
 
226
191
  path = PathSafety.join_under_root(source_root, artifact)
227
192
  raise Error.resolution("package artifact path escapes distribution root") unless path
@@ -233,17 +198,14 @@ module Pray
233
198
  def read_artifact_bytes(source_url, artifact)
234
199
  return read_local_artifact_bytes(source_url, artifact) if local_source?(source_url)
235
200
 
201
+ reject_absolute_artifact!(artifact)
236
202
  http_get(join_url(source_url, artifact))
237
203
  end
238
204
 
239
- def cache_ready?(cache_directory, selected)
240
- return false unless File.directory?(cache_directory)
205
+ def reject_absolute_artifact!(artifact)
206
+ return unless artifact.match?(%r{\A[a-z][a-z0-9+.-]*:}i)
241
207
 
242
- spec_path = Resolve.find_prayspec_file(cache_directory)
243
- spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
244
- spec.version == selected.version
245
- rescue Errno::ENOENT, Errno::ENOTDIR, SystemCallError
246
- false
208
+ raise Error.integrity("remote artifact path must be relative: #{artifact}")
247
209
  end
248
210
 
249
211
  def registry_metadata_path(source_root, package_name)
@@ -301,12 +263,7 @@ module Pray
301
263
 
302
264
  def http_get(url)
303
265
  uri = URI(url)
304
- response = http_request(uri) { |http| http.get(uri.request_uri) }
305
- unless response.is_a?(Net::HTTPSuccess)
306
- raise Error.resolution("HTTP request failed for #{url}: #{response.code}")
307
- end
308
-
309
- response.body
266
+ bounded_http_exchange(uri, Net::HTTP::Get.new(uri), "HTTP request failed for #{url}")
310
267
  end
311
268
 
312
269
  def http_put(url, content_type, body)
@@ -314,12 +271,7 @@ module Pray
314
271
  request = Net::HTTP::Put.new(uri)
315
272
  request["Content-Type"] = content_type
316
273
  request.body = body
317
- response = http_request(uri) { |http| http.request(request) }
318
- unless response.is_a?(Net::HTTPSuccess)
319
- raise Error.resolution("HTTP upload failed for #{url}: #{response.code}")
320
- end
321
-
322
- response.body
274
+ bounded_http_exchange(uri, request, "HTTP upload failed for #{url}")
323
275
  end
324
276
 
325
277
  def http_post(url, content_type, body)
@@ -327,12 +279,23 @@ module Pray
327
279
  request = Net::HTTP::Post.new(uri)
328
280
  request["Content-Type"] = content_type
329
281
  request.body = body
330
- response = http_request(uri) { |http| http.request(request) }
331
- unless response.is_a?(Net::HTTPSuccess)
332
- raise Error.resolution("HTTP request failed for #{url}: #{response.code}")
282
+ bounded_http_exchange(uri, request, "HTTP request failed for #{url}")
283
+ end
284
+
285
+ def bounded_http_exchange(uri, request, failure_message)
286
+ body = nil
287
+ code = nil
288
+ success = false
289
+ http_request(uri) do |http|
290
+ http.request(request) do |response|
291
+ success = response.is_a?(Net::HTTPSuccess)
292
+ code = response.code
293
+ body = HttpBody.read_response!(response)
294
+ end
333
295
  end
296
+ raise Error.resolution("#{failure_message}: #{code}") unless success
334
297
 
335
- response.body
298
+ body
336
299
  end
337
300
 
338
301
  def http_request(uri)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Pray
6
+ module RegistryInstall
7
+ module_function
8
+
9
+ def install_artifact_to_cache(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
10
+ staging_directory = "#{cache_directory}.staging"
11
+ FileUtils.rm_rf(staging_directory)
12
+ FileUtils.mkdir_p(staging_directory)
13
+ unpacked_directory = File.join(staging_directory, "unpacked")
14
+ FileUtils.mkdir_p(unpacked_directory)
15
+
16
+ begin
17
+ Registry.validate_and_unpack(
18
+ unpacked_directory, declaration, selected, artifact_bytes, source_url: source_url
19
+ )
20
+ FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
21
+ FileUtils.mv(unpacked_directory, cache_directory)
22
+ rescue
23
+ FileUtils.rm_rf(staging_directory)
24
+ raise
25
+ else
26
+ FileUtils.rm_rf(staging_directory)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module RegistryIntegrity
5
+ def validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
6
+ require_integrity_fields!(declaration.name, selected)
7
+ artifact_hash = Hashing.sha256_prefixed(artifact_bytes)
8
+ if artifact_hash != selected.artifact_hash
9
+ raise Error.integrity(
10
+ "package artifact hash mismatch for #{declaration.name} #{selected.version}"
11
+ )
12
+ end
13
+
14
+ verify_registry_signature!(declaration, selected, artifact_bytes)
15
+ Trust.verify_publisher_fingerprint!(source_url, selected) if source_url
16
+ Archive.unpack_praypkg(artifact_bytes, cache_directory)
17
+ spec_path = Resolve.find_prayspec_file(cache_directory)
18
+ spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
19
+ validate_package_identity!(cache_directory, declaration, selected, spec)
20
+ actual_tree_hash = spec.tree_hash_for_root(cache_directory)
21
+ if actual_tree_hash != selected.tree_hash
22
+ raise Error.integrity(
23
+ "package tree hash mismatch for #{declaration.name} #{selected.version}"
24
+ )
25
+ end
26
+ end
27
+
28
+ def require_integrity_fields!(package_name, selected)
29
+ if selected.artifact_hash.to_s.empty?
30
+ raise Error.integrity("package #{package_name} #{selected.version} is missing artifact_hash")
31
+ end
32
+ if selected.tree_hash.to_s.empty?
33
+ raise Error.integrity("package #{package_name} #{selected.version} is missing tree_hash")
34
+ end
35
+ end
36
+
37
+ def cache_ready?(cache_directory, selected)
38
+ return false unless File.directory?(cache_directory)
39
+
40
+ spec_path = Resolve.find_prayspec_file(cache_directory)
41
+ spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
42
+ spec.version == selected.version && !selected.tree_hash.to_s.empty? &&
43
+ spec.tree_hash_for_root(cache_directory) == selected.tree_hash
44
+ rescue Error, SystemCallError
45
+ false
46
+ end
47
+
48
+ private
49
+
50
+ def validate_package_identity!(cache_directory, declaration, selected, spec)
51
+ if spec.name != declaration.name
52
+ raise Error.resolution(
53
+ "package path #{cache_directory.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
54
+ )
55
+ end
56
+ return if spec.version == selected.version
57
+
58
+ raise Error.resolution(
59
+ "package #{declaration.name} version #{spec.version} does not match registry version #{selected.version}"
60
+ )
61
+ end
62
+ end
63
+ end
data/lib/pray/render.rb CHANGED
@@ -18,7 +18,6 @@ module Pray
18
18
  project.manifest.targets.each do |target|
19
19
  output = target.outputs.first
20
20
  next unless output
21
-
22
21
  rendered << render_target(project, target, output)
23
22
  end
24
23
  rendered
@@ -26,6 +25,7 @@ module Pray
26
25
 
27
26
  def write_rendered_targets(project, rendered)
28
27
  rendered.each do |target|
28
+ PathSafety.validate_project_relative_path!(target.path)
29
29
  path = File.join(project.project_root, target.path)
30
30
  FileUtils.mkdir_p(File.dirname(path))
31
31
  File.write(path, target.content)
@@ -36,6 +36,7 @@ module Pray
36
36
  def materialize_provisioned_exports(project)
37
37
  symbols = project.manifest.symbols || {}
38
38
  planned_provisioned_files(project).each do |file|
39
+ PathSafety.validate_project_relative_path!(file.path)
39
40
  destination = File.join(project.project_root, file.path)
40
41
  FileUtils.mkdir_p(File.dirname(destination))
41
42
  write_provisioned_file(file.source, destination, symbols)
@@ -76,7 +77,6 @@ module Pray
76
77
  if target.scoped && target.mode == "compose"
77
78
  return render_scoped_compose(project, target, output)
78
79
  end
79
-
80
80
  render_legacy_compose(project, target, output)
81
81
  end
82
82
 
data/lib/pray/resolve.rb CHANGED
@@ -187,16 +187,16 @@ module Pray
187
187
  return [File.expand_path(local_path, project_root), nil]
188
188
  end
189
189
  return [File.expand_path(declaration.path, project_root), nil] if declaration.path
190
+ source_name = ResolveSource.implied_source_name(declaration, sources)
191
+ if source_name
192
+ source = sources[source_name]
193
+ raise Error.resolution("unknown source: #{source_name}") unless source
190
194
 
191
- if declaration.source
192
- source = sources[declaration.source]
193
- raise Error.resolution("unknown source: #{declaration.source}") unless source
194
-
195
- if (local_path = user_config.local.source[declaration.source])
195
+ if (local_path = user_config.local.source[source_name])
196
196
  source_root = File.expand_path(local_path, project_root)
197
197
  resolved = Registry.resolve_local_registry_package_root(
198
198
  project_root,
199
- "local:#{declaration.source}",
199
+ "local:#{source_name}",
200
200
  source_root,
201
201
  declaration,
202
202
  preferred_version: lockfile_preferred_version(lockfile, declaration.name),
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResolveSource
5
+ module_function
6
+
7
+ def implied_source_name(declaration, sources)
8
+ return declaration.source if declaration.source
9
+
10
+ namespace = package_namespace(declaration.name)
11
+ return namespace if namespace && sources.key?(namespace)
12
+
13
+ case sources.length
14
+ when 0 then nil
15
+ when 1 then sources.keys.first
16
+ else
17
+ raise Error.resolution(
18
+ "package #{declaration.name} requires source: when multiple sources are declared " \
19
+ "and the package namespace does not match a source"
20
+ )
21
+ end
22
+ end
23
+
24
+ def package_namespace(name)
25
+ name.include?("/") ? name.split("/", 2).first : nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResourceLimits
5
+ MAX_ARCHIVE_ENTRIES = 10_000
6
+ MAX_ARCHIVE_ENTRY_BYTES = 32 * 1024 * 1024
7
+ MAX_ARCHIVE_TOTAL_BYTES = 64 * 1024 * 1024
8
+ MAX_HTTP_RESPONSE_BYTES = 64 * 1024 * 1024
9
+ MAX_SERVE_BODY_BYTES = 16 * 1024 * 1024
10
+ MAX_SERVE_HEADER_BYTES = 64 * 1024
11
+ MAX_SERVE_CONCURRENT_CONNECTIONS = 32
12
+ SERVE_SOCKET_TIMEOUT_SECONDS = 30
13
+ end
14
+ end
data/lib/pray/serve.rb CHANGED
@@ -4,12 +4,14 @@ require "socket"
4
4
  require "fileutils"
5
5
  require "pathname"
6
6
  require "json"
7
+ require "timeout"
7
8
  require_relative "path_safety"
9
+ require_relative "resource_limits"
8
10
  require_relative "serve_federation"
9
11
 
10
12
  module Pray
11
13
  module Serve
12
- DEFAULT_MAX_CONNECTIONS = 8
14
+ DEFAULT_MAX_CONNECTIONS = ResourceLimits::MAX_SERVE_CONCURRENT_CONNECTIONS
13
15
 
14
16
  module_function
15
17
 
@@ -39,7 +41,14 @@ module Pray
39
41
  end
40
42
 
41
43
  def serve_connection(root, socket, connection_slots)
42
- handle_connection(root, socket)
44
+ Timeout.timeout(ResourceLimits::SERVE_SOCKET_TIMEOUT_SECONDS) do
45
+ handle_connection(root, socket)
46
+ end
47
+ rescue Timeout::Error
48
+ socket.print(request_timeout) unless socket.closed?
49
+ rescue Error => error
50
+ response = error.message.include?("request body exceeds") ? payload_too_large : bad_request
51
+ socket.print(response) unless socket.closed?
43
52
  ensure
44
53
  connection_slots << true unless connection_slots.closed?
45
54
  socket.close unless socket.closed?
@@ -51,7 +60,9 @@ module Pray
51
60
 
52
61
  method, path, = request_line.split
53
62
  headers = read_headers(socket)
54
- body_length = headers["content-length"].to_i
63
+ body_length = Integer(headers.fetch("content-length", "0"), exception: false)
64
+ raise Error.parse("request", "invalid content length") unless body_length
65
+ validate_body_length!(body_length)
55
66
  body = body_length.positive? ? socket.read(body_length) : ""
56
67
 
57
68
  response = dispatch_request(root, method, path, body)
@@ -60,9 +71,14 @@ module Pray
60
71
 
61
72
  def read_headers(socket)
62
73
  headers = {}
74
+ header_bytes = 0
63
75
  loop do
64
76
  line = socket.gets
65
77
  break if line.nil? || line.strip.empty?
78
+ header_bytes += line.bytesize
79
+ if header_bytes > ResourceLimits::MAX_SERVE_HEADER_BYTES
80
+ raise Error.parse("request", "request headers exceed server limit")
81
+ end
66
82
 
67
83
  name, value = line.split(":", 2)
68
84
  headers[name.strip.downcase] = value.strip if name && value
@@ -70,10 +86,20 @@ module Pray
70
86
  headers
71
87
  end
72
88
 
89
+ def validate_body_length!(body_length)
90
+ return if body_length <= ResourceLimits::MAX_SERVE_BODY_BYTES
91
+
92
+ raise Error.unsupported(
93
+ "request body exceeds #{ResourceLimits::MAX_SERVE_BODY_BYTES} bytes"
94
+ )
95
+ end
96
+
73
97
  def dispatch_request(root, method, path, body = "")
74
98
  path = path.split("?", 2).first
75
99
 
76
100
  case [method, path]
101
+ when ["GET", "/health"]
102
+ return ok_response("text/plain", "ok")
77
103
  when ["GET", "/.well-known/pray-federation.json"]
78
104
  return ServeFederation.discovery_response(root)
79
105
  when ["GET", "/v1/sync/index"]
@@ -89,12 +115,13 @@ module Pray
89
115
  return not_found unless method == "GET"
90
116
 
91
117
  if path == "/"
92
- return html_response("<h1>Pray distribution</h1><p>Root: #{root}</p>")
118
+ return html_response("<h1>Pray distribution</h1>")
93
119
  end
94
120
 
95
121
  file_path = PathSafety.join_under_root(root, path.delete_prefix("/"))
96
122
  return not_found unless file_path
97
123
  return not_found unless File.file?(file_path)
124
+ return payload_too_large if File.size(file_path) > ResourceLimits::MAX_HTTP_RESPONSE_BYTES
98
125
 
99
126
  content_type = content_type_for(file_path)
100
127
  file_body = File.binread(file_path)
@@ -126,5 +153,20 @@ module Pray
126
153
  body = "too many connections"
127
154
  "HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
128
155
  end
156
+
157
+ def request_timeout
158
+ body = "request timed out"
159
+ "HTTP/1.1 408 Request Timeout\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
160
+ end
161
+
162
+ def payload_too_large
163
+ body = "request exceeds server limit"
164
+ "HTTP/1.1 413 Payload Too Large\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
165
+ end
166
+
167
+ def bad_request
168
+ body = "bad request"
169
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
170
+ end
129
171
  end
130
172
  end
data/lib/pray/session.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
+ require_relative "trust"
5
6
 
6
7
  module Pray
7
8
  SessionFile = Struct.new(
@@ -12,13 +13,14 @@ module Pray
12
13
  module Session
13
14
  module_function
14
15
 
15
- def session_file_path(root)
16
- File.join(root, ".pray", "session.json")
16
+ def session_file_path(_root)
17
+ File.join(Trust.trust_home, "session.json")
17
18
  end
18
19
 
19
20
  def persist(root, session)
20
21
  path = session_file_path(root)
21
22
  FileUtils.mkdir_p(File.dirname(path))
23
+ migrate_legacy_session(root, path)
22
24
  sessions = load_sessions(path)
23
25
  existing = sessions.find { |entry| entry.server_url == session.server_url }
24
26
  if existing
@@ -34,12 +36,14 @@ module Pray
34
36
  else
35
37
  {"sessions" => sessions.map { |entry| session_to_hash(entry) }}
36
38
  end
37
- File.write(path, JSON.pretty_generate(document))
39
+ write_document(path, document)
38
40
  session
39
41
  end
40
42
 
41
43
  def load_latest(root)
42
- sessions = load_sessions(session_file_path(root))
44
+ path = session_file_path(root)
45
+ migrate_legacy_session(root, path)
46
+ sessions = load_sessions(path)
43
47
  sessions.reverse.find { |session| !session.email.to_s.strip.empty? }
44
48
  end
45
49
 
@@ -86,5 +90,33 @@ module Pray
86
90
  signer_fingerprint: entry["signer_fingerprint"]
87
91
  )
88
92
  end
93
+
94
+ def migrate_legacy_session(root, path)
95
+ legacy_path = File.join(root, ".pray", "session.json")
96
+ return unless File.file?(legacy_path)
97
+ return if File.expand_path(legacy_path) == File.expand_path(path)
98
+
99
+ sessions = load_sessions(path)
100
+ load_sessions(legacy_path).each do |legacy|
101
+ sessions << legacy unless sessions.any? { |entry| entry.server_url == legacy.server_url }
102
+ end
103
+ document = (sessions.length == 1) ? session_to_hash(sessions.first) : {
104
+ "sessions" => sessions.map { |entry| session_to_hash(entry) }
105
+ }
106
+ FileUtils.mkdir_p(File.dirname(path))
107
+ write_document(path, document)
108
+ File.delete(legacy_path)
109
+ end
110
+
111
+ def write_document(path, document)
112
+ temporary_path = "#{path}.tmp-#{Process.pid}"
113
+ File.open(temporary_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
114
+ file.write("#{JSON.pretty_generate(document)}\n")
115
+ file.flush
116
+ file.fsync
117
+ end
118
+ File.chmod(0o600, temporary_path)
119
+ File.rename(temporary_path, path)
120
+ end
89
121
  end
90
122
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "path_safety"
5
+ require_relative "resource_limits"
6
+
7
+ module Pray
8
+ module TarValidation
9
+ BLOCK_BYTES = 512
10
+ State = Struct.new(:paths, :entry_count, :total_bytes, :pending_path, keyword_init: true)
11
+
12
+ module_function
13
+
14
+ def extract!(bytes, output_directory)
15
+ FileUtils.mkdir_p(output_directory)
16
+ validate!(bytes, output_directory: output_directory)
17
+ end
18
+
19
+ def validate!(bytes, output_directory: nil)
20
+ offset = 0
21
+ state = State.new(paths: {}, entry_count: 0, total_bytes: 0)
22
+
23
+ while offset + BLOCK_BYTES <= bytes.bytesize
24
+ header = bytes.byteslice(offset, BLOCK_BYTES)
25
+ return if header.bytes.all?(&:zero?)
26
+
27
+ verify_checksum!(header)
28
+ size = parse_octal(header.byteslice(124, 12), "entry size")
29
+ data_start = offset + BLOCK_BYTES
30
+ data_end = data_start + size
31
+ raise Error.integrity("truncated package archive entry") if data_end > bytes.bytesize
32
+
33
+ process_entry(header, bytes.byteslice(data_start, size), size, state, output_directory)
34
+ offset = data_start + ((size + BLOCK_BYTES - 1) / BLOCK_BYTES * BLOCK_BYTES)
35
+ end
36
+ raise Error.integrity("package archive is missing its end marker")
37
+ end
38
+
39
+ def process_entry(header, data, size, state, output_directory)
40
+ type = header.byteslice(156, 1)
41
+ if type == "x"
42
+ state.pending_path = pax_path(data)
43
+ return
44
+ end
45
+ if type == "L"
46
+ state.pending_path = c_string(data).delete_suffix("\n")
47
+ return
48
+ end
49
+
50
+ raw_path = state.pending_path || header_path(header)
51
+ state.pending_path = nil
52
+ return if type == "5" && [".", "./"].include?(raw_path)
53
+
54
+ path = PathSafety.validate_archive_member_path!(raw_path)
55
+ return if File.basename(path).start_with?("._")
56
+ if type == "5"
57
+ write_extracted_directory(output_directory, path) if output_directory
58
+ return
59
+ end
60
+ validate_regular_entry!(type, path, size, state)
61
+ write_extracted_file(output_directory, path, data) if output_directory
62
+ end
63
+
64
+ def validate_regular_entry!(type, path, size, state)
65
+ raise Error.integrity("unsupported package archive entry type") unless ["0", "\0"].include?(type)
66
+
67
+ state.entry_count += 1
68
+ if state.entry_count > ResourceLimits::MAX_ARCHIVE_ENTRIES
69
+ raise Error.integrity("package archive exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRIES} entries")
70
+ end
71
+ raise Error.integrity("duplicate package archive path: #{path}") if state.paths.key?(path)
72
+
73
+ state.paths[path] = true
74
+ if size > ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES
75
+ raise Error.integrity("package archive entry exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES} bytes: #{path}")
76
+ end
77
+ state.total_bytes += size
78
+ if state.total_bytes > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
79
+ raise Error.integrity("package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} decompressed bytes")
80
+ end
81
+ end
82
+
83
+ def write_extracted_directory(output_directory, path)
84
+ FileUtils.mkdir_p(extracted_destination(output_directory, path))
85
+ end
86
+
87
+ def write_extracted_file(output_directory, path, data)
88
+ destination = extracted_destination(output_directory, path)
89
+ FileUtils.mkdir_p(File.dirname(destination))
90
+ File.open(destination, File::WRONLY | File::CREAT | File::EXCL | File::BINARY) do |file|
91
+ file.write(data)
92
+ end
93
+ end
94
+
95
+ def extracted_destination(output_directory, path)
96
+ destination = File.join(output_directory, path)
97
+ unless PathSafety.path_under_root?(output_directory, destination)
98
+ raise Error.integrity("package path escapes package root: #{path}")
99
+ end
100
+
101
+ destination
102
+ end
103
+
104
+ def verify_checksum!(header)
105
+ stored = parse_octal(header.byteslice(148, 6), "checksum")
106
+ sum = 0
107
+ header.bytes.each_with_index do |byte, index|
108
+ sum += (index >= 148 && index < 156) ? 32 : byte
109
+ end
110
+ return if sum == stored
111
+
112
+ raise Error.integrity("invalid package archive checksum")
113
+ end
114
+
115
+ def header_path(header)
116
+ name = c_string(header.byteslice(0, 100))
117
+ prefix = c_string(header.byteslice(345, 155))
118
+ prefix.empty? ? name : "#{prefix}/#{name}"
119
+ end
120
+
121
+ def c_string(bytes)
122
+ bytes.to_s.split("\0", 2).first.to_s.force_encoding(Encoding::UTF_8).tap do |text|
123
+ raise Error.integrity("invalid package archive path encoding") unless text.valid_encoding?
124
+ end
125
+ end
126
+
127
+ def parse_octal(bytes, label)
128
+ value = c_string(bytes).strip
129
+ raise Error.integrity("invalid package archive #{label}") unless value.match?(/\A[0-7]+\z/)
130
+
131
+ value.to_i(8)
132
+ end
133
+
134
+ def pax_path(data)
135
+ cursor = 0
136
+ path = nil
137
+ while cursor < data.bytesize
138
+ separator = data.index(" ".b, cursor)
139
+ length = separator && Integer(data.byteslice(cursor, separator - cursor), exception: false)
140
+ record_end = cursor + length.to_i
141
+ if !length || length <= 0 || record_end > data.bytesize
142
+ raise Error.integrity("invalid package archive pax header")
143
+ end
144
+ record = data.byteslice(separator + 1, record_end - separator - 2)
145
+ equals = record.index("=".b)
146
+ if equals && record.byteslice(0, equals) == "path"
147
+ value = record.byteslice(equals + 1, record.bytesize - equals - 1).force_encoding(Encoding::UTF_8)
148
+ raise Error.integrity("invalid package archive path encoding") unless value.valid_encoding?
149
+
150
+ path = value
151
+ end
152
+ cursor = record_end
153
+ end
154
+ path
155
+ end
156
+ end
157
+ end
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.9.0"
5
- GENERATED_BY = "pray 1.9.0"
4
+ VERSION = "1.9.2"
5
+ GENERATED_BY = "pray 1.9.2"
6
6
  end
data/lib/pray.rb CHANGED
@@ -19,12 +19,18 @@ require_relative "pray/dotenv"
19
19
  require_relative "pray/project_context"
20
20
  require_relative "pray/environment"
21
21
  require_relative "pray/resolve_context"
22
+ require_relative "pray/resolve_source"
22
23
  require_relative "pray/invocation"
23
24
  require_relative "pray/resolve"
24
25
  require_relative "pray/render"
25
26
  require_relative "pray/verify_position"
26
27
  require_relative "pray/verify"
28
+ require_relative "pray/resource_limits"
29
+ require_relative "pray/http_body"
30
+ require_relative "pray/registry_install"
31
+ require_relative "pray/registry_integrity"
27
32
  require_relative "pray/registry"
33
+ require_relative "pray/archive_unpack"
28
34
  require_relative "pray/archive"
29
35
  require_relative "pray/plan"
30
36
  require_relative "pray/git_sources"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pray-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -97,6 +97,7 @@ files:
97
97
  - lib/pray-cli.rb
98
98
  - lib/pray.rb
99
99
  - lib/pray/archive.rb
100
+ - lib/pray/archive_unpack.rb
100
101
  - lib/pray/auth_client.rb
101
102
  - lib/pray/cli.rb
102
103
  - lib/pray/cli/commands/auth.rb
@@ -123,11 +124,13 @@ files:
123
124
  - lib/pray/format_serialize.rb
124
125
  - lib/pray/git_sources.rb
125
126
  - lib/pray/hashing.rb
127
+ - lib/pray/http_body.rb
126
128
  - lib/pray/invocation.rb
127
129
  - lib/pray/literal.rb
128
130
  - lib/pray/lockfile.rb
129
131
  - lib/pray/lockfile_serialize.rb
130
132
  - lib/pray/manifest.rb
133
+ - lib/pray/manifest_constraint.rb
131
134
  - lib/pray/manifest_formatter.rb
132
135
  - lib/pray/manifest_json.rb
133
136
  - lib/pray/manifest_parser.rb
@@ -140,9 +143,13 @@ files:
140
143
  - lib/pray/project_context.rb
141
144
  - lib/pray/publish.rb
142
145
  - lib/pray/registry.rb
146
+ - lib/pray/registry_install.rb
147
+ - lib/pray/registry_integrity.rb
143
148
  - lib/pray/render.rb
144
149
  - lib/pray/resolve.rb
145
150
  - lib/pray/resolve_context.rb
151
+ - lib/pray/resolve_source.rb
152
+ - lib/pray/resource_limits.rb
146
153
  - lib/pray/serve.rb
147
154
  - lib/pray/serve_federation.rb
148
155
  - lib/pray/session.rb
@@ -150,6 +157,7 @@ files:
150
157
  - lib/pray/statement_surface.rb
151
158
  - lib/pray/substitute.rb
152
159
  - lib/pray/sync.rb
160
+ - lib/pray/tar_validation.rb
153
161
  - lib/pray/terminal.rb
154
162
  - lib/pray/trust.rb
155
163
  - lib/pray/trust_feed.rb