Skip to content
624 linesCodeBlameRaw
1//! The packages service: the registries a workspace publishes to and
2//! installs from, beside its code (docs/PACKAGES.md). Container images
3//! first, spoken over the OCI Distribution protocol on `g1t.sh/v2/`; npm,
4//! Composer, Cargo, Go, Maven, NuGet and RubyGems after.
5//!
6//! The site reaches it over `POST /rpc/<method>` with the arguments below;
7//! the registries' own protocols are any other request. Mirrors
8//! `packages/contracts/src/packages.ts`.
9//!
10//! Who may do what: a package linked to a repository has that
11//! repository's visibility and, unless its admins turned inheriting off,
12//! its roles (Read pulls, Write publishes, Admin deletes and changes
13//! settings). An unlinked one belongs to its workspace: members by the base
14//! permission, owners administer. Either way the roles given on the package
15//! itself to people and teams ([`PackageAccess`]) add to those. A workflow
16//! job's token reaches a package only from the repository it is linked to,
17//! or from a repository given access under Manage Actions access
18//! ([`ActionsAccess`]). Public packages pull anonymously.
19//!
20//! Deleting a package or a version keeps it, hidden, for
21//! [`RESTORE_DAYS`]: an admin can restore it until then, and its name (or
22//! version) cannot be published again until it is purged.
23
24use serde::{Deserialize, Serialize};
25
26use crate::audit::Surface;
27use crate::{User, Viewer};
28
29/// How long a deleted package or version can be restored, in days, before
30/// the purge removes it for good.
31pub const RESTORE_DAYS: u64 = 30;
32
33/// Which registry a package is in.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum Ecosystem {
37 Container,
38 Npm,
39 Composer,
40 Cargo,
41 Go,
42 Maven,
43 Nuget,
44 Rubygems,
45}
46
47impl Ecosystem {
48 pub const ALL: [Ecosystem; 8] = [
49 Ecosystem::Container,
50 Ecosystem::Npm,
51 Ecosystem::Composer,
52 Ecosystem::Cargo,
53 Ecosystem::Go,
54 Ecosystem::Maven,
55 Ecosystem::Nuget,
56 Ecosystem::Rubygems,
57 ];
58
59 pub fn as_str(self) -> &'static str {
60 match self {
61 Ecosystem::Container => "container",
62 Ecosystem::Npm => "npm",
63 Ecosystem::Composer => "composer",
64 Ecosystem::Cargo => "cargo",
65 Ecosystem::Go => "go",
66 Ecosystem::Maven => "maven",
67 Ecosystem::Nuget => "nuget",
68 Ecosystem::Rubygems => "rubygems",
69 }
70 }
71
72 pub fn parse(text: &str) -> Option<Ecosystem> {
73 Ecosystem::ALL.into_iter().find(|e| e.as_str() == text)
74 }
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum Visibility {
80 Public,
81 #[default]
82 Private,
83}
84
85impl Visibility {
86 pub fn as_str(self) -> &'static str {
87 match self {
88 Visibility::Public => "public",
89 Visibility::Private => "private",
90 }
91 }
92
93 pub fn parse(text: &str) -> Visibility {
94 if text == "public" { Visibility::Public } else { Visibility::Private }
95 }
96}
97
98/// The repository a package is linked to.
99#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100pub struct LinkedRepo {
101 pub id: String,
102 pub namespace: String,
103 pub name: String,
104}
105
106/// A package as listings show it.
107#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct PackageSummary {
109 pub id: String,
110 pub workspace: String,
111 pub ecosystem: Ecosystem,
112 /// Without the workspace: `web` for `g1t.sh/acme/web`.
113 pub name: String,
114 /// What a client is given: `g1t.sh/acme/web` for a container image.
115 pub address: String,
116 /// Linked packages follow their repository's visibility.
117 pub visibility: Visibility,
118 pub repo: Option<LinkedRepo>,
119 pub description: Option<String>,
120 pub versions: u32,
121 /// The newest version's tag (for a container image, `latest` when it
122 /// has one) or version.
123 pub latest: Option<String>,
124 /// Bytes its versions hold, each file counted once.
125 pub size: u64,
126 /// Pulls and installs, counted approximately.
127 pub downloads: u64,
128 pub created_at: String,
129 pub updated_at: String,
130 /// For a linked package: whether it takes its repository's roles. Its
131 /// own grants add to them either way.
132 #[serde(default = "yes")]
133 pub inherit_access: bool,
134 /// Set on a deleted package: when, and by whom (a username).
135 #[serde(default)]
136 pub deleted_at: Option<String>,
137 #[serde(default)]
138 pub deleted_by: Option<String>,
139 /// When a deleted package is purged: it can be restored until then.
140 #[serde(default)]
141 pub purge_at: Option<String>,
142}
143
144fn yes() -> bool {
145 true
146}
147
148/// One version: for a container image, one manifest, by digest.
149#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct PackageVersion {
151 pub id: String,
152 /// A tag, semver or (for container images) the manifest's digest.
153 pub version: String,
154 pub digest: String,
155 /// Bytes of its files: an image's layers, config and manifest.
156 pub size: u64,
157 pub media_type: Option<String>,
158 /// For an OCI artifact: what it is, such as a signature or an SBOM.
159 pub artifact_type: Option<String>,
160 /// For an artifact attached to another version: that version's digest.
161 pub subject: Option<String>,
162 /// For an image index: the platforms it holds, such as `linux/amd64`.
163 pub platforms: Vec<String>,
164 pub tags: Vec<String>,
165 /// The username that published it.
166 pub published_by: Option<String>,
167 pub published_at: String,
168 /// npm: why the version should no longer be used, when it is deprecated.
169 #[serde(default)]
170 pub deprecated: Option<String>,
171 /// NuGet: whether a symbol package (`.snupkg`) was pushed for it.
172 #[serde(default)]
173 pub symbols: bool,
174 /// Its own pulls or downloads, counted approximately.
175 #[serde(default)]
176 pub downloads: Option<u64>,
177 /// Set on a deleted version: when, and by whom (a username).
178 #[serde(default)]
179 pub deleted_at: Option<String>,
180 #[serde(default)]
181 pub deleted_by: Option<String>,
182 /// When a deleted version is purged: it can be restored until then.
183 #[serde(default)]
184 pub purge_at: Option<String>,
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize)]
188pub struct PackageTag {
189 pub tag: String,
190 pub digest: String,
191 pub updated_at: String,
192}
193
194/// What the viewer may do with a package.
195#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
196pub struct PackagePermissions {
197 pub pull: bool,
198 pub push: bool,
199 /// Delete and restore it and its versions.
200 pub delete: bool,
201 /// Change its settings: access, Actions access, visibility and link.
202 pub admin: bool,
203}
204
205/// A role on a package. Read pulls, Write publishes, Admin deletes,
206/// restores and changes its settings.
207#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum PackageRole {
210 Read,
211 Write,
212 Admin,
213}
214
215impl PackageRole {
216 pub fn as_str(self) -> &'static str {
217 match self {
218 PackageRole::Read => "read",
219 PackageRole::Write => "write",
220 PackageRole::Admin => "admin",
221 }
222 }
223
224 pub fn parse(text: &str) -> Option<PackageRole> {
225 match text.trim().to_ascii_lowercase().as_str() {
226 "read" | "pull" => Some(PackageRole::Read),
227 "write" | "push" => Some(PackageRole::Write),
228 "admin" => Some(PackageRole::Admin),
229 _ => None,
230 }
231 }
232
233 /// "Read", for sentences.
234 pub fn label(self) -> &'static str {
235 match self {
236 PackageRole::Read => "Read",
237 PackageRole::Write => "Write",
238 PackageRole::Admin => "Admin",
239 }
240 }
241}
242
243/// Who a [`PackageAccess`] is for.
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum GranteeKind {
247 User,
248 Team,
249}
250
251impl GranteeKind {
252 pub fn as_str(self) -> &'static str {
253 match self {
254 GranteeKind::User => "user",
255 GranteeKind::Team => "team",
256 }
257 }
258
259 pub fn parse(text: &str) -> Option<GranteeKind> {
260 match text {
261 "user" => Some(GranteeKind::User),
262 "team" => Some(GranteeKind::Team),
263 _ => None,
264 }
265 }
266}
267
268/// A person or a team with a role on a package itself.
269#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
270pub struct PackageAccess {
271 pub kind: GranteeKind,
272 /// The person's or the team's id.
273 pub id: String,
274 /// A username, or a team as `workspace/slug`.
275 pub name: String,
276 pub role: PackageRole,
277 pub created_at: String,
278}
279
280/// A repository whose workflow jobs may use a package (Manage Actions
281/// access). The repository a package is linked to is listed with `linked`
282/// set: its jobs may always publish it.
283#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
284pub struct ActionsAccess {
285 pub repo_id: String,
286 /// `owner/name`.
287 pub repo: String,
288 /// Read or Write.
289 pub role: PackageRole,
290 #[serde(default)]
291 pub linked: bool,
292 #[serde(default)]
293 pub created_at: Option<String>,
294}
295
296/// `package_settings`: what a package's admins see on its Settings tab.
297#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct PackageSettings {
299 pub package: PackageSummary,
300 pub access: Vec<PackageAccess>,
301 pub actions_access: Vec<ActionsAccess>,
302 /// Its deleted versions that can still be restored, newest first.
303 pub deleted_versions: Vec<PackageVersion>,
304 pub permissions: PackagePermissions,
305}
306
307/// `get_package`.
308#[derive(Clone, Debug, Serialize, Deserialize)]
309pub struct PackageDetail {
310 pub package: PackageSummary,
311 /// Newest first.
312 pub versions: Vec<PackageVersion>,
313 pub tags: Vec<PackageTag>,
314 pub permissions: PackagePermissions,
315 /// The package's README, as markdown: npm's, from its latest version.
316 #[serde(default)]
317 pub readme: Option<String>,
318}
319
320/// `list_packages`: the packages in a workspace the viewer may pull, newest
321/// first. Returns `Outcome<Vec<PackageSummary>>`.
322#[derive(Clone, Debug, Default, Serialize, Deserialize)]
323pub struct ListPackagesArgs {
324 pub workspace: String,
325 pub viewer: Viewer,
326 #[serde(default)]
327 pub ecosystem: Option<Ecosystem>,
328 /// Only those linked to this repository.
329 #[serde(default)]
330 pub repo_id: Option<String>,
331 /// Matched against names.
332 #[serde(default)]
333 pub query: Option<String>,
334}
335
336/// `get_package`. Returns `Outcome<PackageDetail>`; not found when the
337/// viewer may not pull it.
338#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct GetPackageArgs {
340 pub workspace: String,
341 pub ecosystem: Ecosystem,
342 pub name: String,
343 pub viewer: Viewer,
344}
345
346/// `delete_version`: a version, by its id, version or digest, or by a tag
347/// that points to it. It is hidden at once and can be restored for
348/// [`RESTORE_DAYS`]; its tags come back with it unless they were moved
349/// meanwhile. Needs Admin. Returns `Outcome<()>`.
350#[derive(Clone, Debug, Serialize, Deserialize)]
351pub struct DeleteVersionArgs {
352 pub actor: User,
353 pub workspace: String,
354 pub ecosystem: Ecosystem,
355 pub name: String,
356 pub version: String,
357 #[serde(default)]
358 pub surface: Option<Surface>,
359}
360
361/// `delete_package`: a package and every version, hidden at once and
362/// restorable for [`RESTORE_DAYS`]; its name stays taken until then.
363/// Needs Admin. Returns `Outcome<()>`.
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct DeletePackageArgs {
366 pub actor: User,
367 pub workspace: String,
368 pub ecosystem: Ecosystem,
369 pub name: String,
370 #[serde(default)]
371 pub surface: Option<Surface>,
372}
373
374/// `set_package`: change a package's visibility, the repository it is
375/// linked to, or whether it takes that repository's roles. `link` names a
376/// repository of its workspace (Admin on it is needed too); `unlink` takes
377/// the link away (the package is then the workspace's, and private until
378/// someone makes it public). A linked package's visibility is its
379/// repository's, so `visibility` is refused for one. Needs Admin. Returns
380/// `Outcome<PackageSummary>`.
381#[derive(Clone, Debug, Serialize, Deserialize)]
382pub struct SetPackageArgs {
383 pub actor: User,
384 pub workspace: String,
385 pub ecosystem: Ecosystem,
386 pub name: String,
387 #[serde(default)]
388 pub visibility: Option<Visibility>,
389 #[serde(default)]
390 pub link: Option<String>,
391 #[serde(default)]
392 pub unlink: bool,
393 /// For a linked package: whether it takes its repository's roles.
394 #[serde(default)]
395 pub inherit_access: Option<bool>,
396 #[serde(default)]
397 pub surface: Option<Surface>,
398}
399
400/// `package_settings`: a package's access, Actions access and deleted
401/// versions. Admins only; not found for anyone who may not pull it.
402/// Returns `Outcome<PackageSettings>`.
403#[derive(Clone, Debug, Serialize, Deserialize)]
404pub struct PackageSettingsArgs {
405 pub workspace: String,
406 pub ecosystem: Ecosystem,
407 pub name: String,
408 pub viewer: Viewer,
409}
410
411/// `list_versions`: a package's versions, newest first: active ones, or
412/// with `deleted` the deleted ones that can still be restored (admins
413/// only). Returns `Outcome<Vec<PackageVersion>>`.
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct ListVersionsArgs {
416 pub workspace: String,
417 pub ecosystem: Ecosystem,
418 pub name: String,
419 pub viewer: Viewer,
420 #[serde(default)]
421 pub deleted: bool,
422}
423
424/// `get_version`: one version by its id, version, digest or a tag that
425/// points to it. Returns `Outcome<PackageVersion>`.
426#[derive(Clone, Debug, Serialize, Deserialize)]
427pub struct GetVersionArgs {
428 pub workspace: String,
429 pub ecosystem: Ecosystem,
430 pub name: String,
431 pub viewer: Viewer,
432 pub version: String,
433}
434
435/// `restore_package`: a deleted package, with every version it had when it
436/// was deleted, while it can still be restored. Needs Admin, as it was.
437/// Returns `Outcome<PackageSummary>`.
438#[derive(Clone, Debug, Serialize, Deserialize)]
439pub struct RestorePackageArgs {
440 pub actor: User,
441 pub workspace: String,
442 pub ecosystem: Ecosystem,
443 pub name: String,
444 #[serde(default)]
445 pub surface: Option<Surface>,
446}
447
448/// `restore_version`: a deleted version, by its id or version, while it
449/// can still be restored. Needs Admin. Returns `Outcome<PackageVersion>`.
450#[derive(Clone, Debug, Serialize, Deserialize)]
451pub struct RestoreVersionArgs {
452 pub actor: User,
453 pub workspace: String,
454 pub ecosystem: Ecosystem,
455 pub name: String,
456 pub version: String,
457 #[serde(default)]
458 pub surface: Option<Surface>,
459}
460
461/// `deleted_packages`: a workspace's deleted packages that can still be
462/// restored, newest deletion first, those the viewer administers. Returns
463/// `Outcome<Vec<PackageSummary>>`.
464#[derive(Clone, Debug, Default, Serialize, Deserialize)]
465pub struct DeletedPackagesArgs {
466 pub workspace: String,
467 pub viewer: Viewer,
468}
469
470/// `set_package_access`: give a person (`user`, a username) or a team
471/// (`team`, its slug or `workspace/slug`) a role on the package, or change
472/// theirs. Needs Admin. Returns `Outcome<Vec<PackageAccess>>`.
473#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct SetPackageAccessArgs {
475 pub actor: User,
476 pub workspace: String,
477 pub ecosystem: Ecosystem,
478 pub name: String,
479 #[serde(default)]
480 pub user: Option<String>,
481 #[serde(default)]
482 pub team: Option<String>,
483 pub role: PackageRole,
484 #[serde(default)]
485 pub surface: Option<Surface>,
486}
487
488/// `remove_package_access`: take a person's or a team's role on the
489/// package away. Needs Admin. Returns `Outcome<Vec<PackageAccess>>`.
490#[derive(Clone, Debug, Serialize, Deserialize)]
491pub struct RemovePackageAccessArgs {
492 pub actor: User,
493 pub workspace: String,
494 pub ecosystem: Ecosystem,
495 pub name: String,
496 #[serde(default)]
497 pub user: Option<String>,
498 #[serde(default)]
499 pub team: Option<String>,
500 #[serde(default)]
501 pub surface: Option<Surface>,
502}
503
504/// `set_actions_access`: let a repository of the package's workspace
505/// (`repo`, its name or `owner/name`) use the package from its workflows,
506/// with the Read or Write role. Needs Admin. Returns
507/// `Outcome<Vec<ActionsAccess>>`.
508#[derive(Clone, Debug, Serialize, Deserialize)]
509pub struct SetActionsAccessArgs {
510 pub actor: User,
511 pub workspace: String,
512 pub ecosystem: Ecosystem,
513 pub name: String,
514 pub repo: String,
515 pub role: PackageRole,
516 #[serde(default)]
517 pub surface: Option<Surface>,
518}
519
520/// `remove_actions_access`: stop a repository's workflows using the
521/// package. The linked repository cannot be removed: unlink the package
522/// instead. Needs Admin. Returns `Outcome<Vec<ActionsAccess>>`.
523#[derive(Clone, Debug, Serialize, Deserialize)]
524pub struct RemoveActionsAccessArgs {
525 pub actor: User,
526 pub workspace: String,
527 pub ecosystem: Ecosystem,
528 pub name: String,
529 pub repo: String,
530 #[serde(default)]
531 pub surface: Option<Surface>,
532}
533
534/// `storage`: what a workspace's packages hold, each file counted once, as
535/// public when any public package uses it. For billing. Returns
536/// [`PackageStorage`].
537#[derive(Clone, Debug, Serialize, Deserialize)]
538pub struct StorageArgs {
539 pub workspace: String,
540}
541
542/// `sync_composer`: read a repository's Composer package again now, as a
543/// push would: made, updated or deleted from its branches, tags and
544/// `composer.json`. Returns `bool`: whether it is a package.
545#[derive(Clone, Debug, Serialize, Deserialize)]
546pub struct SyncComposerArgs {
547 pub repo_id: String,
548}
549
550#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
551pub struct PackageStorage {
552 pub public_bytes: u64,
553 pub private_bytes: u64,
554}
555
556/// `storage_all`: [`PackageStorage`] for every workspace that has
557/// packages, from one query, for billing's daily measure. Takes `{}`;
558/// returns `Vec<WorkspacePackageStorage>`, by workspace.
559#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
560pub struct WorkspacePackageStorage {
561 pub workspace: String,
562 pub public_bytes: u64,
563 pub private_bytes: u64,
564}
565
566/// Every RPC method of the service, as the site's mirror lists them.
567pub const METHODS: [&str; 18] = [
568 "list_packages",
569 "get_package",
570 "list_versions",
571 "get_version",
572 "delete_version",
573 "delete_package",
574 "restore_version",
575 "restore_package",
576 "deleted_packages",
577 "set_package",
578 "package_settings",
579 "set_package_access",
580 "remove_package_access",
581 "set_actions_access",
582 "remove_actions_access",
583 "storage",
584 "storage_all",
585 "sync_composer",
586];
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn roles_read_back_and_order() {
594 for role in [PackageRole::Read, PackageRole::Write, PackageRole::Admin] {
595 assert_eq!(PackageRole::parse(role.as_str()), Some(role));
596 assert_eq!(serde_json::to_value(role).unwrap(), role.as_str());
597 }
598 assert!(PackageRole::Read < PackageRole::Write && PackageRole::Write < PackageRole::Admin);
599 assert_eq!(PackageRole::parse("maintain"), None);
600 }
601
602 #[test]
603 fn ecosystems_read_back() {
604 for ecosystem in Ecosystem::ALL {
605 assert_eq!(Ecosystem::parse(ecosystem.as_str()), Some(ecosystem));
606 assert_eq!(serde_json::to_value(ecosystem).unwrap(), ecosystem.as_str());
607 }
608 assert_eq!(Visibility::parse("public"), Visibility::Public);
609 assert_eq!(Visibility::parse("anything"), Visibility::Private);
610 }
611
612 /// The site's copy, `packages/contracts/src/packages.ts`, names the
613 /// same ecosystems and methods.
614 #[test]
615 fn the_typescript_mirror_names_the_same_ecosystems_and_methods() {
616 let ts = include_str!("../../../packages/contracts/src/packages.ts");
617 for ecosystem in Ecosystem::ALL {
618 assert!(ts.contains(&format!("\"{}\"", ecosystem.as_str())), "{}", ecosystem.as_str());
619 }
620 for method in METHODS {
621 assert!(ts.contains(&format!("\"{method}\"")), "{method}");
622 }
623 }
624}