pray-cli 1.6.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.md +21 -0
- data/README.md +125 -0
- data/SECURITY.md +28 -0
- data/bin/polyrun +108 -0
- data/bin/pray +26 -0
- data/lib/pray/archive.rb +68 -0
- data/lib/pray/auth_client.rb +97 -0
- data/lib/pray/cli/commands/auth.rb +42 -0
- data/lib/pray/cli/commands/distribution.rb +17 -0
- data/lib/pray/cli/commands/init.rb +62 -0
- data/lib/pray/cli/commands/meta.rb +15 -0
- data/lib/pray/cli/commands/packages.rb +107 -0
- data/lib/pray/cli/commands/trust.rb +73 -0
- data/lib/pray/cli/commands/workflow.rb +126 -0
- data/lib/pray/cli/help.rb +168 -0
- data/lib/pray/cli/helpers.rb +166 -0
- data/lib/pray/cli/parse.rb +136 -0
- data/lib/pray/cli/parse_auth.rb +91 -0
- data/lib/pray/cli/parse_trust.rb +152 -0
- data/lib/pray/cli/suggest.rb +57 -0
- data/lib/pray/cli.rb +117 -0
- data/lib/pray/confess.rb +113 -0
- data/lib/pray/config.rb +52 -0
- data/lib/pray/constraint.rb +80 -0
- data/lib/pray/destination.rb +158 -0
- data/lib/pray/dotenv.rb +46 -0
- data/lib/pray/environment.rb +41 -0
- data/lib/pray/error.rb +55 -0
- data/lib/pray/format_manifest.rb +255 -0
- data/lib/pray/format_serialize.rb +135 -0
- data/lib/pray/git_sources.rb +228 -0
- data/lib/pray/hashing.rb +59 -0
- data/lib/pray/invocation.rb +110 -0
- data/lib/pray/literal.rb +382 -0
- data/lib/pray/lockfile.rb +259 -0
- data/lib/pray/lockfile_serialize.rb +125 -0
- data/lib/pray/manifest.rb +126 -0
- data/lib/pray/manifest_formatter.rb +56 -0
- data/lib/pray/manifest_json.rb +128 -0
- data/lib/pray/manifest_parser.rb +152 -0
- data/lib/pray/manifest_parser_blocks.rb +216 -0
- data/lib/pray/manifest_parser_helpers.rb +208 -0
- data/lib/pray/materialize.rb +120 -0
- data/lib/pray/package_spec.rb +245 -0
- data/lib/pray/path_safety.rb +41 -0
- data/lib/pray/plan.rb +83 -0
- data/lib/pray/project_context.rb +67 -0
- data/lib/pray/publish.rb +162 -0
- data/lib/pray/registry.rb +355 -0
- data/lib/pray/render.rb +360 -0
- data/lib/pray/resolve.rb +396 -0
- data/lib/pray/resolve_context.rb +19 -0
- data/lib/pray/serve.rb +130 -0
- data/lib/pray/serve_federation.rb +109 -0
- data/lib/pray/session.rb +90 -0
- data/lib/pray/ssh_agent.rb +115 -0
- data/lib/pray/statement_surface.rb +262 -0
- data/lib/pray/substitute.rb +50 -0
- data/lib/pray/sync.rb +219 -0
- data/lib/pray/terminal.rb +30 -0
- data/lib/pray/trust.rb +201 -0
- data/lib/pray/trust_feed.rb +110 -0
- data/lib/pray/trust_ops.rb +202 -0
- data/lib/pray/verify.rb +257 -0
- data/lib/pray/version.rb +6 -0
- data/lib/pray-cli.rb +4 -0
- data/lib/pray.rb +46 -0
- data/pray-cli.gemspec +48 -0
- metadata +187 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require_relative "path_safety"
|
|
9
|
+
|
|
10
|
+
module Pray
|
|
11
|
+
RegistryPackageVersion = Struct.new(
|
|
12
|
+
:version, :artifact, :artifact_hash, :tree_hash, :yanked, :targets, :exports,
|
|
13
|
+
:signer, :signer_fingerprint, :published_at, :signature
|
|
14
|
+
) do
|
|
15
|
+
def initialize(
|
|
16
|
+
artifact_hash: nil, tree_hash: nil, yanked: false, targets: [], exports: [],
|
|
17
|
+
signer: nil, signer_fingerprint: nil, published_at: nil, signature: nil, **kwargs
|
|
18
|
+
)
|
|
19
|
+
super(**kwargs, artifact_hash: artifact_hash, tree_hash: tree_hash, yanked: yanked,
|
|
20
|
+
targets: targets, exports: exports, signer: signer,
|
|
21
|
+
signer_fingerprint: signer_fingerprint, published_at: published_at, signature: signature)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RegistryPackageMetadata = Struct.new(:name, :versions) do
|
|
26
|
+
def initialize(name: nil, versions: [])
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
RegistryPackageResolution = Struct.new(
|
|
32
|
+
:root, :signer_fingerprint, :registry_latest_version
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
module Registry
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def resolve_registry_package_root(project_root, source_url, declaration, preferred_version: nil, offline: false)
|
|
39
|
+
metadata = fetch_package_metadata(source_url, declaration.name)
|
|
40
|
+
registry_latest_version = registry_latest_version_label(metadata)
|
|
41
|
+
selected = select_package_version(metadata, declaration.constraint, preferred_version)
|
|
42
|
+
cache_directory = registry_cache_directory(
|
|
43
|
+
project_root,
|
|
44
|
+
source_url,
|
|
45
|
+
declaration.name,
|
|
46
|
+
selected.version,
|
|
47
|
+
selected.artifact_hash
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if cache_ready?(cache_directory, selected)
|
|
51
|
+
return RegistryPackageResolution.new(
|
|
52
|
+
root: cache_directory,
|
|
53
|
+
signer_fingerprint: selected.signer_fingerprint,
|
|
54
|
+
registry_latest_version: registry_latest_version
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
|
|
59
|
+
|
|
60
|
+
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
61
|
+
FileUtils.mkdir_p(cache_directory)
|
|
62
|
+
|
|
63
|
+
artifact_bytes = read_artifact_bytes(source_url, selected.artifact)
|
|
64
|
+
validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: source_url)
|
|
65
|
+
|
|
66
|
+
RegistryPackageResolution.new(
|
|
67
|
+
root: cache_directory,
|
|
68
|
+
signer_fingerprint: selected.signer_fingerprint,
|
|
69
|
+
registry_latest_version: registry_latest_version
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolve_local_registry_package_root(project_root, source_key, source_root, declaration, preferred_version: nil, offline: false)
|
|
74
|
+
metadata_path = registry_metadata_path(source_root, declaration.name)
|
|
75
|
+
unless File.exist?(metadata_path)
|
|
76
|
+
raise Error.resolution(
|
|
77
|
+
"package #{declaration.name} not found in distribution #{source_root.inspect}. " \
|
|
78
|
+
"Missing #{metadata_path}. Check the package name, version constraint `#{declaration.constraint}`, " \
|
|
79
|
+
"and that the source publishes registry metadata."
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
metadata = parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
|
|
84
|
+
registry_latest_version = registry_latest_version_label(metadata)
|
|
85
|
+
selected = select_package_version(metadata, declaration.constraint, preferred_version)
|
|
86
|
+
cache_directory = registry_cache_directory(
|
|
87
|
+
project_root,
|
|
88
|
+
source_key,
|
|
89
|
+
declaration.name,
|
|
90
|
+
selected.version,
|
|
91
|
+
selected.artifact_hash
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if cache_ready?(cache_directory, selected)
|
|
95
|
+
return RegistryPackageResolution.new(
|
|
96
|
+
root: cache_directory,
|
|
97
|
+
signer_fingerprint: selected.signer_fingerprint,
|
|
98
|
+
registry_latest_version: registry_latest_version
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
|
|
103
|
+
|
|
104
|
+
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
105
|
+
FileUtils.mkdir_p(cache_directory)
|
|
106
|
+
|
|
107
|
+
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
|
+
|
|
110
|
+
RegistryPackageResolution.new(
|
|
111
|
+
root: cache_directory,
|
|
112
|
+
signer_fingerprint: selected.signer_fingerprint,
|
|
113
|
+
registry_latest_version: registry_latest_version
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fetch_package_metadata(source_url, package_name)
|
|
118
|
+
if local_source?(source_url)
|
|
119
|
+
metadata_path = registry_metadata_path(source_url, package_name)
|
|
120
|
+
return parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
PathSafety.reject_unsafe_package_name!(package_name)
|
|
124
|
+
response = http_get(join_url(source_url, "v1/packages/#{package_name}.json"))
|
|
125
|
+
parse_metadata(response)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_metadata(text)
|
|
129
|
+
data = JSON.parse(text)
|
|
130
|
+
RegistryPackageMetadata.new(
|
|
131
|
+
name: data["name"],
|
|
132
|
+
versions: Array(data["versions"]).map { |entry| version_from_hash(entry) }
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def version_from_hash(entry)
|
|
137
|
+
RegistryPackageVersion.new(
|
|
138
|
+
version: entry["version"],
|
|
139
|
+
artifact: entry["artifact"],
|
|
140
|
+
artifact_hash: entry["artifact_hash"],
|
|
141
|
+
tree_hash: entry["tree_hash"],
|
|
142
|
+
yanked: entry["yanked"] || false,
|
|
143
|
+
targets: entry["targets"] || [],
|
|
144
|
+
exports: entry["exports"] || [],
|
|
145
|
+
signer: entry["signer"],
|
|
146
|
+
signer_fingerprint: entry["signer_fingerprint"],
|
|
147
|
+
published_at: entry["published_at"],
|
|
148
|
+
signature: entry["signature"]
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def select_package_version(metadata, constraint, preferred_version)
|
|
153
|
+
if preferred_version
|
|
154
|
+
version = metadata.versions.find { |entry| entry.version == preferred_version && !entry.yanked }
|
|
155
|
+
return version if version && Constraint.version_satisfies(version.version, constraint)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
selected = nil
|
|
159
|
+
metadata.versions.each do |version|
|
|
160
|
+
next if version.yanked
|
|
161
|
+
next unless Constraint.version_satisfies(version.version, constraint)
|
|
162
|
+
|
|
163
|
+
selected = version if selected.nil? || compare_versions(version.version, selected.version).positive?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
raise Error.resolution("no registry version for #{metadata.name} satisfies #{constraint}") unless selected
|
|
167
|
+
|
|
168
|
+
selected
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def registry_latest_version_label(metadata)
|
|
172
|
+
highest = metadata.versions.reject(&:yanked).max_by { |entry| Gem::Version.new(entry.version) }
|
|
173
|
+
highest&.version
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def compare_versions(left, right)
|
|
177
|
+
Gem::Version.new(left) <=> Gem::Version.new(right)
|
|
178
|
+
end
|
|
179
|
+
|
|
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
|
+
def read_local_artifact_bytes(source_root, artifact)
|
|
217
|
+
if artifact.start_with?("http://", "https://")
|
|
218
|
+
return http_get(artifact)
|
|
219
|
+
end
|
|
220
|
+
if artifact.start_with?("file://")
|
|
221
|
+
path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
|
|
222
|
+
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
223
|
+
return File.binread(path)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
path = PathSafety.join_under_root(source_root, artifact)
|
|
227
|
+
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
228
|
+
raise Error.resolution("package artifact missing at #{path}") unless File.exist?(path)
|
|
229
|
+
|
|
230
|
+
File.binread(path)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def read_artifact_bytes(source_url, artifact)
|
|
234
|
+
return read_local_artifact_bytes(source_url, artifact) if local_source?(source_url)
|
|
235
|
+
|
|
236
|
+
http_get(join_url(source_url, artifact))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cache_ready?(cache_directory, selected)
|
|
240
|
+
return false unless File.directory?(cache_directory)
|
|
241
|
+
|
|
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
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def registry_metadata_path(source_root, package_name)
|
|
250
|
+
PathSafety.reject_unsafe_package_name!(package_name)
|
|
251
|
+
packages_root = PathSafety.join_under_root(source_root, "v1", "packages")
|
|
252
|
+
raise Error.resolution("invalid package name: #{package_name.inspect}") unless packages_root
|
|
253
|
+
|
|
254
|
+
metadata_path = Pathname.new(packages_root).join("#{package_name}.json").cleanpath.to_s
|
|
255
|
+
unless PathSafety.path_under_root?(packages_root, metadata_path)
|
|
256
|
+
raise Error.resolution("invalid package name: #{package_name.inspect}")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
metadata_path
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def verify_registry_signature!(declaration, selected, artifact_bytes)
|
|
263
|
+
return unless selected.signature
|
|
264
|
+
|
|
265
|
+
unless selected.signer && selected.tree_hash
|
|
266
|
+
raise Error.integrity(
|
|
267
|
+
"package signature metadata incomplete for #{declaration.name} #{selected.version}"
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
expected = registry_artifact_signature(artifact_bytes, selected.tree_hash, selected.signer)
|
|
272
|
+
return if expected == selected.signature
|
|
273
|
+
|
|
274
|
+
raise Error.integrity(
|
|
275
|
+
"package signature mismatch for #{declaration.name} #{selected.version}"
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def registry_cache_directory(project_root, source_key, package_name, version, artifact_hash)
|
|
280
|
+
identifier = [
|
|
281
|
+
source_key,
|
|
282
|
+
package_name,
|
|
283
|
+
version,
|
|
284
|
+
artifact_hash || "no-artifact-hash"
|
|
285
|
+
].join(":")
|
|
286
|
+
digest = Hashing.sha256_hex(identifier)[0, 16]
|
|
287
|
+
File.join(project_root, ".pray", "cache", "registry", package_name.tr("/", "-"), version, digest)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def offline_package_error(package_name, version)
|
|
291
|
+
"package #{package_name} #{version} is not cached and offline mode is enabled"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def local_source?(source_url)
|
|
295
|
+
!source_url.start_with?("http://", "https://", "pray+ssh://", "ssh+pray://")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def join_url(base, path)
|
|
299
|
+
URI.join(base.end_with?("/") ? base : "#{base}/", path.delete_prefix("/")).to_s
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def http_get(url)
|
|
303
|
+
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
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def http_put(url, content_type, body)
|
|
313
|
+
uri = URI(url)
|
|
314
|
+
request = Net::HTTP::Put.new(uri)
|
|
315
|
+
request["Content-Type"] = content_type
|
|
316
|
+
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
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def http_post(url, content_type, body)
|
|
326
|
+
uri = URI(url)
|
|
327
|
+
request = Net::HTTP::Post.new(uri)
|
|
328
|
+
request["Content-Type"] = content_type
|
|
329
|
+
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}")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
response.body
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def http_request(uri)
|
|
339
|
+
Net::HTTP.start(
|
|
340
|
+
uri.hostname,
|
|
341
|
+
uri.port,
|
|
342
|
+
use_ssl: uri.scheme == "https",
|
|
343
|
+
open_timeout: 10,
|
|
344
|
+
read_timeout: 30
|
|
345
|
+
) do |http|
|
|
346
|
+
yield http
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def registry_artifact_signature(artifact_bytes, tree_hash, signer)
|
|
351
|
+
payload = artifact_bytes + "\0".b + tree_hash.b + "\0".b + signer.b
|
|
352
|
+
Hashing.sha256_prefixed(payload)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|