mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
* start consolidating * add start-cli flash-os * combine install and setup and refactor all * use http * undo mock * fix translation * translations * use dialogservice wrapper * better ST messaging on setup * only warn on update if breakages (#3097) * finish setup wizard and ui language-keyboard feature * fix typo * wip: localization * remove start-tunnel readme * switch to posix strings for language internal * revert mock * translate backend strings * fix missing about text * help text for args * feat: add "Add new gateway" option (#3098) * feat: add "Add new gateway" option * Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add translation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix dns selection * keyboard keymap also * ability to shutdown after install * revert mock * working setup flow + manifest localization * (mostly) redundant localization on frontend * version bump * omit live medium from disk list and better space management * ignore missing package archive on 035 migration * fix device migration * add i18n helper to sdk * fix install over 0.3.5.1 * fix grub config --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
510 lines
16 KiB
Rust
510 lines
16 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use clap::Parser;
|
|
use imbl_value::InternedString;
|
|
use itertools::Itertools;
|
|
use rpc_toolkit::HandlerArgs;
|
|
use serde::{Deserialize, Serialize};
|
|
use ts_rs::TS;
|
|
use url::Url;
|
|
|
|
use crate::PackageId;
|
|
use crate::context::CliContext;
|
|
use crate::prelude::*;
|
|
use crate::progress::FullProgressTracker;
|
|
use crate::registry::asset::BufferedHttpSource;
|
|
use crate::registry::context::RegistryContext;
|
|
use crate::registry::package::index::PackageVersionInfo;
|
|
use crate::s9pk::S9pk;
|
|
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
|
use crate::s9pk::v2::SIG_CONTEXT;
|
|
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
|
use crate::sign::ed25519::Ed25519;
|
|
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
|
use crate::util::VersionString;
|
|
use crate::util::io::TrackingIO;
|
|
use crate::util::serde::Base64;
|
|
|
|
#[derive(Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
pub struct AddPackageParams {
|
|
#[ts(type = "string[]")]
|
|
pub urls: Vec<Url>,
|
|
#[ts(skip)]
|
|
#[serde(rename = "__Auth_signer")]
|
|
pub uploader: AnyVerifyingKey,
|
|
pub commitment: MerkleArchiveCommitment,
|
|
pub signature: AnySignature,
|
|
}
|
|
|
|
pub async fn add_package(
|
|
ctx: RegistryContext,
|
|
AddPackageParams {
|
|
urls,
|
|
uploader,
|
|
commitment,
|
|
signature,
|
|
}: AddPackageParams,
|
|
) -> Result<(), Error> {
|
|
uploader
|
|
.scheme()
|
|
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
|
|
let peek = ctx.db.peek().await;
|
|
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
|
|
|
|
let Some(([url], rest)) = urls.split_at_checked(1) else {
|
|
return Err(Error::new(
|
|
eyre!("{}", t!("registry.package.add.must-specify-url")),
|
|
ErrorKind::InvalidRequest,
|
|
));
|
|
};
|
|
|
|
let s9pk = S9pk::deserialize(
|
|
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
|
|
Some(&commitment),
|
|
)
|
|
.await?;
|
|
|
|
for url in rest {
|
|
S9pk::deserialize(
|
|
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
|
|
Some(&commitment),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
let manifest = s9pk.as_manifest();
|
|
|
|
let mut info = PackageVersionInfo::from_s9pk(&s9pk, urls).await?;
|
|
for (_, s9pk) in &mut info.s9pks {
|
|
if !s9pk.signatures.contains_key(&uploader) && s9pk.commitment == commitment {
|
|
s9pk.signatures.insert(uploader.clone(), signature.clone());
|
|
}
|
|
}
|
|
|
|
ctx.db
|
|
.mutate(|db| {
|
|
if db.as_admins().de()?.contains(&uploader_guid)
|
|
|| db
|
|
.as_index()
|
|
.as_package()
|
|
.as_packages()
|
|
.as_idx(&manifest.id)
|
|
.or_not_found(&manifest.id)?
|
|
.as_authorized()
|
|
.de()?
|
|
.get(&uploader_guid)
|
|
.map_or(false, |v| manifest.version.satisfies(v))
|
|
{
|
|
let package = db
|
|
.as_index_mut()
|
|
.as_package_mut()
|
|
.as_packages_mut()
|
|
.upsert(&manifest.id, || Ok(Default::default()))?;
|
|
let v = package.as_versions_mut();
|
|
if let Some(prev) = v.as_idx_mut(&manifest.version) {
|
|
prev.mutate(|p| p.merge_with(info, true))?;
|
|
} else {
|
|
v.insert(&manifest.version, &info)?;
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(Error::new(eyre!("{}", t!("registry.package.add.unauthorized")), ErrorKind::Authorization))
|
|
}
|
|
})
|
|
.await
|
|
.result
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Parser)]
|
|
#[command(rename_all = "kebab-case")]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliAddPackageParams {
|
|
#[arg(help = "help.arg.s9pk-file-path")]
|
|
pub file: PathBuf,
|
|
#[arg(long, help = "help.arg.package-url")]
|
|
pub url: Vec<Url>,
|
|
#[arg(long, help = "help.arg.no-verify")]
|
|
pub no_verify: bool,
|
|
}
|
|
|
|
pub async fn cli_add_package(
|
|
HandlerArgs {
|
|
context: ctx,
|
|
parent_method,
|
|
method,
|
|
params:
|
|
CliAddPackageParams {
|
|
file,
|
|
url,
|
|
no_verify,
|
|
},
|
|
..
|
|
}: HandlerArgs<CliContext, CliAddPackageParams>,
|
|
) -> Result<(), Error> {
|
|
let s9pk = S9pk::open(&file, None).await?;
|
|
|
|
let progress = FullProgressTracker::new();
|
|
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
|
let verify = if !no_verify {
|
|
url.iter()
|
|
.map(|url| {
|
|
let phase = progress.add_phase(
|
|
InternedString::from_display(&lazy_format!("Verifying {url}")),
|
|
Some(100),
|
|
);
|
|
(url.clone(), phase)
|
|
})
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let mut index_phase = progress.add_phase(
|
|
InternedString::intern("Adding File to Registry Index"),
|
|
Some(1),
|
|
);
|
|
|
|
let progress_task =
|
|
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
|
|
|
sign_phase.start();
|
|
let commitment = s9pk.as_archive().commitment().await?;
|
|
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
|
|
sign_phase.complete();
|
|
|
|
for (url, mut phase) in verify {
|
|
phase.start();
|
|
let source = BufferedHttpSource::new(ctx.client.clone(), url, phase).await?;
|
|
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
|
|
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true)
|
|
.await?;
|
|
}
|
|
|
|
index_phase.start();
|
|
ctx.call_remote::<RegistryContext>(
|
|
&parent_method.into_iter().chain(method).join("."),
|
|
imbl_value::json!({
|
|
"urls": &url,
|
|
"signature": AnySignature::Ed25519(signature),
|
|
"commitment": commitment,
|
|
}),
|
|
)
|
|
.await?;
|
|
index_phase.complete();
|
|
|
|
progress.complete();
|
|
|
|
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
pub struct RemovePackageParams {
|
|
#[arg(help = "help.arg.package-id")]
|
|
pub id: PackageId,
|
|
#[arg(help = "help.arg.package-version")]
|
|
pub version: VersionString,
|
|
#[arg(long, help = "help.arg.signature-hash")]
|
|
pub sighash: Option<Base64<[u8; 32]>>,
|
|
#[ts(skip)]
|
|
#[arg(skip)]
|
|
#[serde(rename = "__Auth_signer")]
|
|
pub signer: Option<AnyVerifyingKey>,
|
|
}
|
|
|
|
pub async fn remove_package(
|
|
ctx: RegistryContext,
|
|
RemovePackageParams {
|
|
id,
|
|
version,
|
|
sighash,
|
|
signer,
|
|
}: RemovePackageParams,
|
|
) -> Result<bool, Error> {
|
|
let peek = ctx.db.peek().await;
|
|
let signer =
|
|
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
|
|
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
|
|
|
|
let rev = ctx
|
|
.db
|
|
.mutate(|db| {
|
|
if db.as_admins().de()?.contains(&signer_guid)
|
|
|| db
|
|
.as_index()
|
|
.as_package()
|
|
.as_packages()
|
|
.as_idx(&id)
|
|
.or_not_found(&id)?
|
|
.as_authorized()
|
|
.de()?
|
|
.get(&signer_guid)
|
|
.map_or(false, |v| version.satisfies(v))
|
|
{
|
|
if let Some(package) = db
|
|
.as_index_mut()
|
|
.as_package_mut()
|
|
.as_packages_mut()
|
|
.as_idx_mut(&id)
|
|
{
|
|
if let Some(sighash) = sighash {
|
|
if if let Some(package) = package.as_versions_mut().as_idx_mut(&version) {
|
|
package.as_s9pks_mut().mutate(|s| {
|
|
s.retain(|(_, asset)| asset.commitment.root_sighash != sighash);
|
|
Ok(s.is_empty())
|
|
})?
|
|
} else {
|
|
false
|
|
} {
|
|
package.as_versions_mut().remove(&version)?;
|
|
}
|
|
} else {
|
|
package.as_versions_mut().remove(&version)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(Error::new(eyre!("{}", t!("registry.package.unauthorized")), ErrorKind::Authorization))
|
|
}
|
|
})
|
|
.await;
|
|
rev.result.map(|_| rev.revision.is_some())
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
pub struct AddMirrorParams {
|
|
#[ts(type = "string")]
|
|
pub url: Url,
|
|
#[ts(skip)]
|
|
#[serde(rename = "__Auth_signer")]
|
|
pub uploader: AnyVerifyingKey,
|
|
pub commitment: MerkleArchiveCommitment,
|
|
pub signature: AnySignature,
|
|
}
|
|
|
|
pub async fn add_mirror(
|
|
ctx: RegistryContext,
|
|
AddMirrorParams {
|
|
url,
|
|
uploader,
|
|
commitment,
|
|
signature,
|
|
}: AddMirrorParams,
|
|
) -> Result<(), Error> {
|
|
uploader
|
|
.scheme()
|
|
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
|
|
let peek = ctx.db.peek().await;
|
|
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
|
|
|
|
let s9pk = S9pk::deserialize(
|
|
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
|
|
Some(&commitment),
|
|
)
|
|
.await?;
|
|
|
|
let manifest = s9pk.as_manifest();
|
|
|
|
let mut info = PackageVersionInfo::from_s9pk(&s9pk, vec![url]).await?;
|
|
for (_, s9pk) in &mut info.s9pks {
|
|
if !s9pk.signatures.contains_key(&uploader) && s9pk.commitment == commitment {
|
|
s9pk.signatures.insert(uploader.clone(), signature.clone());
|
|
}
|
|
}
|
|
|
|
ctx.db
|
|
.mutate(|db| {
|
|
if db.as_admins().de()?.contains(&uploader_guid)
|
|
|| db
|
|
.as_index()
|
|
.as_package()
|
|
.as_packages()
|
|
.as_idx(&manifest.id)
|
|
.or_not_found(&manifest.id)?
|
|
.as_authorized()
|
|
.de()?
|
|
.get(&uploader_guid)
|
|
.map_or(false, |v| manifest.version.satisfies(v))
|
|
{
|
|
let package = db
|
|
.as_index_mut()
|
|
.as_package_mut()
|
|
.as_packages_mut()
|
|
.as_idx_mut(&manifest.id)
|
|
.and_then(|p| p.as_versions_mut().as_idx_mut(&manifest.version))
|
|
.or_not_found(&lazy_format!("{}@{}", &manifest.id, &manifest.version))?;
|
|
package.mutate(|p| p.merge_with(info, false))?;
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(Error::new(eyre!("{}", t!("registry.package.add-mirror.unauthorized")), ErrorKind::Authorization))
|
|
}
|
|
})
|
|
.await
|
|
.result
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Parser)]
|
|
#[command(rename_all = "kebab-case")]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliAddMirrorParams {
|
|
#[arg(help = "help.arg.s9pk-file-path")]
|
|
pub file: PathBuf,
|
|
#[arg(help = "help.arg.mirror-url")]
|
|
pub url: Url,
|
|
#[arg(long, help = "help.arg.no-verify")]
|
|
pub no_verify: bool,
|
|
}
|
|
|
|
pub async fn cli_add_mirror(
|
|
HandlerArgs {
|
|
context: ctx,
|
|
parent_method,
|
|
method,
|
|
params:
|
|
CliAddMirrorParams {
|
|
file,
|
|
url,
|
|
no_verify,
|
|
},
|
|
..
|
|
}: HandlerArgs<CliContext, CliAddMirrorParams>,
|
|
) -> Result<(), Error> {
|
|
let s9pk = S9pk::open(&file, None).await?;
|
|
|
|
let progress = FullProgressTracker::new();
|
|
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
|
let verify = if !no_verify {
|
|
let url = &url;
|
|
vec![(
|
|
url.clone(),
|
|
progress.add_phase(
|
|
InternedString::from_display(&lazy_format!("Verifying {url}")),
|
|
Some(100),
|
|
),
|
|
)]
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let mut index_phase = progress.add_phase(
|
|
InternedString::intern("Adding File to Registry Index"),
|
|
Some(1),
|
|
);
|
|
|
|
let progress_task =
|
|
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
|
|
|
sign_phase.start();
|
|
let commitment = s9pk.as_archive().commitment().await?;
|
|
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
|
|
sign_phase.complete();
|
|
|
|
for (url, mut phase) in verify {
|
|
phase.start();
|
|
let source = BufferedHttpSource::new(ctx.client.clone(), url, phase).await?;
|
|
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
|
|
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true)
|
|
.await?;
|
|
}
|
|
|
|
index_phase.start();
|
|
ctx.call_remote::<RegistryContext>(
|
|
&parent_method.into_iter().chain(method).join("."),
|
|
imbl_value::json!({
|
|
"url": &url,
|
|
"signature": AnySignature::Ed25519(signature),
|
|
"commitment": commitment,
|
|
}),
|
|
)
|
|
.await?;
|
|
index_phase.complete();
|
|
|
|
progress.complete();
|
|
|
|
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
pub struct RemoveMirrorParams {
|
|
#[arg(help = "help.arg.package-id")]
|
|
pub id: PackageId,
|
|
#[arg(help = "help.arg.package-version")]
|
|
pub version: VersionString,
|
|
#[arg(long, help = "help.arg.mirror-url")]
|
|
#[ts(type = "string")]
|
|
pub url: Url,
|
|
#[ts(skip)]
|
|
#[arg(skip)]
|
|
#[serde(rename = "__Auth_signer")]
|
|
pub signer: Option<AnyVerifyingKey>,
|
|
}
|
|
|
|
pub async fn remove_mirror(
|
|
ctx: RegistryContext,
|
|
RemoveMirrorParams {
|
|
id,
|
|
version,
|
|
url,
|
|
signer,
|
|
}: RemoveMirrorParams,
|
|
) -> Result<(), Error> {
|
|
let peek = ctx.db.peek().await;
|
|
let signer =
|
|
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
|
|
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
|
|
|
|
ctx.db
|
|
.mutate(|db| {
|
|
if db.as_admins().de()?.contains(&signer_guid)
|
|
|| db
|
|
.as_index()
|
|
.as_package()
|
|
.as_packages()
|
|
.as_idx(&id)
|
|
.or_not_found(&id)?
|
|
.as_authorized()
|
|
.de()?
|
|
.get(&signer_guid)
|
|
.map_or(false, |v| version.satisfies(v))
|
|
{
|
|
if let Some(package) = db
|
|
.as_index_mut()
|
|
.as_package_mut()
|
|
.as_packages_mut()
|
|
.as_idx_mut(&id)
|
|
.and_then(|p| p.as_versions_mut().as_idx_mut(&version))
|
|
{
|
|
package.as_s9pks_mut().mutate(|s| {
|
|
s.iter_mut()
|
|
.for_each(|(_, asset)| asset.urls.retain(|u| u != &url));
|
|
if s.iter().any(|(_, asset)| asset.urls.is_empty()) {
|
|
Err(Error::new(
|
|
eyre!("{}", t!("registry.package.cannot-remove-last-mirror")),
|
|
ErrorKind::InvalidRequest,
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})?;
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(Error::new(eyre!("{}", t!("registry.package.remove-mirror.unauthorized")), ErrorKind::Authorization))
|
|
}
|
|
})
|
|
.await
|
|
.result
|
|
}
|