fixes from testing, client side device filtering for better fingerprinting resistance

This commit is contained in:
Aiden McClelland
2026-01-14 18:04:14 -07:00
parent 359c7a89bf
commit ffa801ff6d
29 changed files with 933 additions and 682 deletions

View File

@@ -4,7 +4,7 @@ parse_essential_db_info() {
DB_DUMP="/tmp/startos_db.json"
if command -v start-cli >/dev/null 2>&1; then
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
timeout 30 start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
else
return 1
fi

661
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ use std::sync::Arc;
use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore;
use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString;
use josekit::jwk::Jwk;
use once_cell::sync::OnceCell;
@@ -238,10 +239,16 @@ impl CliContext {
where
Self: CallRemote<RemoteContext>,
{
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {})
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
&self,
method,
OrdMap::new(),
params,
Empty {},
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
}
pub async fn call_remote_with<RemoteContext, T>(
&self,
@@ -252,10 +259,16 @@ impl CliContext {
where
Self: CallRemote<RemoteContext, T>,
{
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
<Self as CallRemote<RemoteContext, T>>::call_remote(
&self,
method,
OrdMap::new(),
params,
extra,
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
}
}
impl AsRef<Jwk> for CliContext {
@@ -292,7 +305,13 @@ impl AsRef<Client> for CliContext {
}
impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
@@ -319,7 +338,13 @@ impl CallRemote<RpcContext> for CliContext {
}
}
impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
@@ -332,7 +357,13 @@ impl CallRemote<DiagnosticContext> for CliContext {
}
}
impl CallRemote<InitContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
@@ -345,7 +376,13 @@ impl CallRemote<InitContext> for CliContext {
}
}
impl CallRemote<SetupContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
@@ -358,7 +395,13 @@ impl CallRemote<SetupContext> for CliContext {
}
}
impl CallRemote<InstallContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),

View File

@@ -29,7 +29,6 @@ use crate::db::model::package::TaskSeverity;
use crate::disk::OsPartitionInfo;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::loop_dev::LoopDev;
use crate::disk::mount::filesystem::{FileSystem, ReadOnly};
use crate::disk::mount::guard::MountGuard;
use crate::init::{InitResult, check_time_is_synchronized};
@@ -586,8 +585,14 @@ impl RpcContext {
where
Self: CallRemote<RemoteContext>,
{
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {})
.await
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
&self,
method,
OrdMap::new(),
params,
Empty {},
)
.await
}
pub async fn call_remote_with<RemoteContext, T>(
&self,
@@ -598,7 +603,14 @@ impl RpcContext {
where
Self: CallRemote<RemoteContext, T>,
{
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await
<Self as CallRemote<RemoteContext, T>>::call_remote(
&self,
method,
OrdMap::new(),
params,
extra,
)
.await
}
}
impl AsRef<Client> for RpcContext {

View File

@@ -416,6 +416,51 @@ impl<T: Map> Model<T> {
}
}
impl<T: Map> Model<T>
where
T::Key: FromStr,
Error: From<<T::Key as FromStr>::Err>,
{
/// Retains only the elements specified by the predicate.
/// The predicate can mutate the values and returns whether to keep each entry.
pub fn retain<F>(&mut self, mut f: F) -> Result<(), Error>
where
F: FnMut(&T::Key, &mut Model<T::Value>) -> Result<bool, Error>,
{
let mut to_remove = Vec::new();
match &mut self.value {
Value::Object(o) => {
for (k, v) in o.iter_mut() {
let key = T::Key::from_str(&**k)?;
if !f(&key, patch_db::ModelExt::value_as_mut(v))? {
to_remove.push(k.clone());
}
}
}
v => {
use serde::de::Error;
return Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!(
"expected object found {v}"
)),
kind: patch_db::value::ErrorKind::Deserialization,
}
.into());
}
}
// Remove entries that didn't pass the filter
if let Value::Object(o) = &mut self.value {
for k in to_remove {
o.remove(&k);
}
}
Ok(())
}
}
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct JsonKey<T>(pub T);

View File

@@ -7,6 +7,7 @@ use chrono::Utc;
use clap::Parser;
use cookie::{Cookie, Expiration, SameSite};
use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString;
use patch_db::PatchDb;
use patch_db::json_ptr::ROOT;
@@ -171,6 +172,7 @@ impl CallRemote<RegistryContext> for CliContext {
async fn call_remote(
&self,
mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
@@ -240,14 +242,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
async fn call_remote(
&self,
mut method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value,
RegistryUrlParams { mut registry }: RegistryUrlParams,
) -> Result<Value, RpcError> {
let mut headers = HeaderMap::new();
headers.insert(
DEVICE_INFO_HEADER,
DeviceInfo::load(self).await?.to_header_value(),
);
let mut device_info = None;
if metadata
.get("get_device_info")
.and_then(|m| m.as_bool())
.unwrap_or(false)
{
let di = DeviceInfo::load(self).await?;
headers.insert(DEVICE_INFO_HEADER, di.to_header_value());
device_info = Some(di);
}
registry
.path_segments_mut()
@@ -258,15 +267,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from);
crate::middleware::auth::signature::call_remote(
let mut res = crate::middleware::auth::signature::call_remote(
self,
registry,
headers,
sig_context.as_deref(),
method,
params,
params.clone(),
)
.await
.await?;
if let Some(device_info) = device_info {
device_info.filter_for_hardware(method, params, &mut res)?;
}
Ok(res)
}
}

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap;
use std::convert::identity;
use std::ops::Deref;
use axum::extract::Request;
@@ -7,6 +6,8 @@ use axum::response::Response;
use exver::{Version, VersionRange};
use http::HeaderValue;
use imbl_value::InternedString;
use patch_db::ModelExt;
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -15,8 +16,13 @@ use url::Url;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfoMap;
use crate::registry::package::get::{
GetPackageParams, GetPackageResponse, GetPackageResponseFull, PackageDetailLevel,
};
use crate::registry::package::index::PackageVersionInfo;
use crate::util::VersionString;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::lshw::LshwDevice;
use crate::version::VersionT;
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
@@ -25,13 +31,13 @@ pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
pub os: OsInfo,
pub hardware: HardwareInfo,
pub hardware: Option<HardwareInfo>,
}
impl DeviceInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
Ok(Self {
os: OsInfo::from(ctx),
hardware: HardwareInfo::load(ctx).await?,
hardware: Some(HardwareInfo::load(ctx).await?),
})
}
}
@@ -41,21 +47,13 @@ impl DeviceInfo {
url.query_pairs_mut()
.append_pair("os.version", &self.os.version.to_string())
.append_pair("os.compat", &self.os.compat.to_string())
.append_pair("os.platform", &*self.os.platform)
.append_pair("hardware.arch", &*self.hardware.arch)
.append_pair("hardware.ram", &self.hardware.ram.to_string());
for device in &self.hardware.devices {
url.query_pairs_mut().append_pair(
&format!("hardware.device.{}", device.class()),
device.product(),
);
}
.append_pair("os.platform", &*self.os.platform);
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
}
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
Ok(Self {
os: OsInfo {
version: query
@@ -69,35 +67,120 @@ impl DeviceInfo {
.deref()
.into(),
},
hardware: HardwareInfo {
arch: query
.get("hardware.arch")
.or_not_found("hardware.arch")?
.parse()?,
ram: query
.get("hardware.ram")
.or_not_found("hardware.ram")?
.parse()?,
devices: identity(query)
.split_off("hardware.device.")
.into_iter()
.filter_map(|(k, v)| match k.strip_prefix("hardware.device.") {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor {
product: v.into_owned(),
})),
Some("display") => Some(LshwDevice::Display(LshwDisplay {
product: v.into_owned(),
})),
Some(class) => {
tracing::warn!("unknown device class: {class}");
None
}
_ => None,
hardware: has_hw_info
.then(|| {
Ok::<_, Error>(HardwareInfo {
arch: query
.get("hardware.arch")
.or_not_found("hardware.arch")?
.parse()?,
ram: query
.get("hardware.ram")
.or_not_found("hardware.ram")?
.parse()?,
devices: None,
})
.collect(),
},
})
.transpose()?,
})
}
pub fn filter_for_hardware(
&self,
method: &str,
params: Value,
res: &mut Value,
) -> Result<(), Error> {
match method {
"package.get" => {
let params: Model<GetPackageParams> = ModelExt::from_value(params);
let other = params.as_other_versions().de()?;
if params.as_id().transpose_ref().is_some() {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(res))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(res))?;
}
} else {
for (_, v) in res.as_object_mut().into_iter().flat_map(|o| o.iter_mut()) {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(v))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(v))?;
}
}
}
Ok(())
}
"os.version.get" => self.filter_os_version(ModelExt::value_as_mut(res)),
_ => Ok(()),
}
}
fn filter_package_versions(
&self,
versions: &mut Model<BTreeMap<VersionString, PackageVersionInfo>>,
) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter package versions using for_device
versions.retain(|_, info| info.for_device(self))?;
// Alpha.17 compatibility: add legacy fields
if self.os.version <= alpha_17 {
for (_, info) in versions.as_entries_mut()? {
let v = info.as_value_mut();
if let Some(mut tup) = v["s9pks"].get(0).cloned() {
v["s9pk"] = tup[1].take();
v["hardwareRequirements"] = tup[0].take();
v["s9pk"]["url"] = v["s9pk"]["urls"][0].clone();
}
}
}
Ok(())
}
fn filter_package_get(&self, res: &mut Model<GetPackageResponse>) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())
}
fn filter_package_get_full(
&self,
res: &mut Model<GetPackageResponseFull>,
) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())?;
self.filter_package_versions(res.as_other_versions_mut())
}
fn filter_os_version(&self, res: &mut Model<OsVersionInfoMap>) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter OS versions based on source_version compatibility
res.retain(|_, info| {
let source_version = info.as_source_version().de()?;
Ok(self.os.version.satisfies(&source_version))
})?;
// Alpha.17 compatibility: add url field from urls array
if self.os.version <= alpha_17 {
for (_, info) in res.as_entries_mut()? {
let v = info.as_value_mut();
for asset_ty in ["iso", "squashfs", "img"] {
for (_, asset) in v[asset_ty]
.as_object_mut()
.into_iter()
.flat_map(|o| o.iter_mut())
{
asset["url"] = asset["urls"][0].clone();
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -127,7 +210,7 @@ pub struct HardwareInfo {
pub arch: InternedString,
#[ts(type = "number")]
pub ram: u64,
pub devices: Vec<LshwDevice>,
pub devices: Option<Vec<LshwDevice>>,
}
impl HardwareInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
@@ -135,7 +218,7 @@ impl HardwareInfo {
Ok(Self {
arch: s.as_arch().de()?,
ram: s.as_ram().de()?,
devices: s.as_devices().de()?,
devices: Some(s.as_devices().de()?),
})
}
}
@@ -148,11 +231,17 @@ pub struct Metadata {
#[derive(Clone)]
pub struct DeviceInfoMiddleware {
device_info: Option<HeaderValue>,
device_info_header: Option<HeaderValue>,
device_info: Option<DeviceInfo>,
req: Option<RpcRequest>,
}
impl DeviceInfoMiddleware {
pub fn new() -> Self {
Self { device_info: None }
Self {
device_info_header: None,
device_info: None,
req: None,
}
}
}
@@ -163,7 +252,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
_: &RegistryContext,
request: &mut Request,
) -> Result<(), Response> {
self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER);
self.device_info_header = request.headers_mut().remove(DEVICE_INFO_HEADER);
Ok(())
}
async fn process_rpc_request(
@@ -174,9 +263,11 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
) -> Result<(), RpcResponse> {
async move {
if metadata.get_device_info {
if let Some(device_info) = &self.device_info {
request.params["__DeviceInfo_device_info"] =
to_value(&DeviceInfo::from_header_value(device_info)?)?;
if let Some(device_info) = &self.device_info_header {
let device_info = DeviceInfo::from_header_value(device_info)?;
request.params["__DeviceInfo_device_info"] = to_value(&device_info)?;
self.device_info = Some(device_info);
self.req = Some(request.clone());
}
}
@@ -185,4 +276,19 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
.await
.map_err(|e| RpcResponse::from_result(Err(e)))
}
async fn process_rpc_response(
&mut self,
_: &RegistryContext,
response: &mut RpcResponse,
) -> () {
if let (Some(req), Some(device_info), Ok(res)) =
(&self.req, &self.device_info, &mut response.result)
{
if let Err(e) =
device_info.filter_for_hardware(req.method.as_str(), req.params.clone(), res)
{
response.result = Err(e).map_err(From::from);
}
}
}
}

View File

@@ -54,7 +54,7 @@ pub async fn add_package(
let peek = ctx.db.peek().await;
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
let ([url], rest) = urls.split_at(1) else {
let Some(([url], rest)) = urls.split_at_checked(1) else {
return Err(Error::new(
eyre!("must specify at least 1 url"),
ErrorKind::InvalidRequest,

View File

@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use clap::{Parser, ValueEnum};
use exver::{ExtendedVersion, Version, VersionRange};
use exver::{ExtendedVersion, VersionRange};
use imbl_value::{InternedString, json};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
@@ -12,14 +12,11 @@ use crate::PackageId;
use crate::context::CliContext;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo;
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
use crate::s9pk::manifest::HardwareRequirements;
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::util::VersionString;
use crate::util::io::{TrackingIO, to_tmp_path};
use crate::util::serde::{WithIoFormat, display_serializable};
@@ -28,7 +25,7 @@ use crate::util::tui::{choose, choose_custom_display};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "kebab-case")]
#[ts(export)]
pub enum PackageDetailLevel {
None,
@@ -48,10 +45,11 @@ pub struct PackageInfoShort {
pub release_notes: String,
}
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
#[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
#[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageParams {
pub id: Option<PackageId>,
#[ts(type = "string | null")]
@@ -63,14 +61,14 @@ pub struct GetPackageParams {
#[arg(skip)]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
#[serde(default)]
#[arg(default_value = "none")]
pub other_versions: PackageDetailLevel,
pub other_versions: Option<PackageDetailLevel>,
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponse {
#[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>,
@@ -111,9 +109,10 @@ impl GetPackageResponse {
}
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponseFull {
#[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>,
@@ -137,23 +136,15 @@ impl GetPackageResponseFull {
pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>;
pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>;
fn get_matching_models<'a>(
db: &'a Model<PackageIndex>,
fn get_matching_models(
db: &Model<PackageIndex>,
GetPackageParams {
id,
source_version,
device_info,
..
}: &GetPackageParams,
) -> Result<
Vec<(
PackageId,
ExtendedVersion,
&'a Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
)>,
Error,
> {
) -> Result<Vec<(PackageId, ExtendedVersion, Model<PackageVersionInfo>)>, Error> {
if let Some(id) = id {
if let Some(pkg) = db.as_packages().as_idx(id) {
vec![(id.clone(), pkg)]
@@ -180,12 +171,16 @@ fn get_matching_models<'a>(
),
)
})? {
let mut info = info.clone();
if let Some(device_info) = &device_info {
info.for_device(device_info)?
if info.for_device(device_info)? {
Some((k.clone(), ExtendedVersion::from(v), info))
} else {
None
}
} else {
Some(info.as_s9pks().de()?)
Some((k.clone(), ExtendedVersion::from(v), info))
}
.map(|assets| (k.clone(), ExtendedVersion::from(v), info, assets))
} else {
None
},
@@ -199,31 +194,12 @@ fn get_matching_models<'a>(
}
pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> {
use patch_db::ModelExt;
let peek = ctx.db.peek().await;
let mut best: BTreeMap<
PackageId,
BTreeMap<
VersionString,
(
&Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
),
>,
> = Default::default();
let mut other: BTreeMap<
PackageId,
BTreeMap<
VersionString,
(
&Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
),
>,
> = Default::default();
for (id, version, info, assets) in get_matching_models(&peek.as_index().as_package(), &params)?
{
let mut best: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
Default::default();
let mut other: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
Default::default();
for (id, version, info) in get_matching_models(&peek.as_index().as_package(), &params)? {
let package_best = best.entry(id.clone()).or_default();
let package_other = other.entry(id.clone()).or_default();
if params
@@ -242,12 +218,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
package_other.insert(worse_version, info);
}
}
package_best.insert(version.into(), (info, assets));
package_best.insert(version.into(), info);
} else {
package_other.insert(version.into(), (info, assets));
package_other.insert(version.into(), info);
}
}
let mut res = if let Some(id) = &params.id {
if let Some(id) = &params.id {
let categories = peek
.as_index()
.as_package()
@@ -256,23 +232,14 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.map(|p| p.as_categories().de())
.transpose()?
.unwrap_or_default();
let best = best
let best: BTreeMap<VersionString, PackageVersionInfo> = best
.remove(id)
.unwrap_or_default()
.into_iter()
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?;
let other = other.remove(id).unwrap_or_default();
match params.other_versions {
match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value(&GetPackageResponse {
categories,
best,
@@ -284,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
other_versions: Some(
other
.into_iter()
.map(|(k, (i, _))| from_value(i.as_value().clone()).map(|v| (k, v)))
.map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
.try_collect()?,
),
}),
@@ -293,21 +260,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
best,
other_versions: other
.into_iter()
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?,
}),
}
} else {
match params.other_versions {
match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value(
&best
.into_iter()
@@ -326,9 +284,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories,
best: best
.into_iter()
.map(|(k, (i, _))| {
from_value(i.as_value().clone()).map(|v| (k, v))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?,
other_versions: None,
},
@@ -355,24 +311,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories,
best: best
.into_iter()
.into_iter()
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?,
other_versions: Some(
other
.into_iter()
.map(|(k, (i, _))| {
from_value(i.as_value().clone()).map(|v| (k, v))
})
.map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
.try_collect()?,
),
},
@@ -399,31 +343,11 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories,
best: best
.into_iter()
.into_iter()
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?,
other_versions: other
.into_iter()
.into_iter()
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?,
},
))
@@ -431,47 +355,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.try_collect::<_, GetPackagesResponseFull, _>()?,
),
}
}?;
// TODO: remove
if true
|| params.device_info.map_or(false, |d| {
"0.4.0-alpha.17"
.parse::<Version>()
.map_or(false, |v| d.os.version <= v)
})
{
let patch = |v: &mut Value| {
for kind in ["best", "otherVersions"] {
for (_, v) in v[kind]
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
let Some(mut tup) = v["s9pks"].get(0).cloned() else {
continue;
};
v["s9pk"] = tup[1].take();
v["hardwareRequirements"] = tup[0].take();
v["s9pk"]["url"] = v["s9pk"]["urls"][0].clone();
}
}
};
if params.id.is_some() {
patch(&mut res);
} else {
for (_, v) in res
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
patch(v);
}
}
}
Ok(res)
}
pub fn display_package_info(
@@ -483,7 +367,7 @@ pub fn display_package_info(
}
if let Some(_) = params.rest.id {
if params.rest.other_versions == PackageDetailLevel::Full {
if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for table in from_value::<GetPackageResponseFull>(info)?.tables() {
table.print_tty(false)?;
println!();
@@ -495,7 +379,7 @@ pub fn display_package_info(
}
}
} else {
if params.rest.other_versions == PackageDetailLevel::Full {
if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for (_, package) in from_value::<GetPackagesResponseFull>(info)? {
for table in package.tables() {
table.print_tty(false)?;
@@ -620,7 +504,7 @@ pub async fn cli_download(
} else {
"Devices"
},
hw.device.iter().map(|d| &d.pattern_description).join(", ")
hw.device.iter().map(|d| &d.description).join(", ")
)
.unwrap();
}

View File

@@ -1,8 +1,10 @@
use std::collections::{BTreeMap, BTreeSet};
use std::u32;
use chrono::Utc;
use exver::{Version, VersionRange};
use imbl_value::InternedString;
use patch_db::ModelExt;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
@@ -223,50 +225,64 @@ impl PackageVersionInfo {
}
}
impl Model<PackageVersionInfo> {
pub fn for_device(
&self,
device_info: &DeviceInfo,
) -> Result<Option<Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>>, Error>
{
/// Filters this package version for compatibility with the given device.
/// Returns false if the package is incompatible (should be removed).
/// Modifies s9pks in place to only include compatible variants.
pub fn for_device(&mut self, device_info: &DeviceInfo) -> Result<bool, Error> {
if !self
.as_metadata()
.as_os_version()
.de()?
.satisfies(&device_info.os.compat)
{
return Ok(None);
return Ok(false);
}
let mut s9pk = self.as_s9pks().de()?;
s9pk.retain(|(hw, _)| {
if let Some(arch) = &hw.arch {
if !arch.contains(&device_info.hardware.arch) {
return false;
if let Some(hw) = &device_info.hardware {
self.as_s9pks_mut().mutate(|s9pks| {
s9pks.retain(|(hw_req, _)| {
if let Some(arch) = &hw_req.arch {
if !arch.contains(&hw.arch) {
return false;
}
}
if let Some(ram) = hw_req.ram {
if hw.ram < ram {
return false;
}
}
if let Some(dev) = &hw.devices {
for device_filter in &hw_req.device {
if !dev
.iter()
.filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.matches(d))
{
return false;
}
}
}
true
});
if hw.devices.is_some() {
s9pks.sort_by_key(|(req, _)| req.specificity_desc());
} else {
s9pks.sort_by_key(|(req, _)| {
let (dev, arch, ram) = req.specificity_desc();
(u32::MAX - dev, arch, ram)
});
}
}
if let Some(ram) = hw.ram {
if device_info.hardware.ram < ram {
return false;
}
}
for device_filter in &hw.device {
if !device_info
.hardware
.devices
.iter()
.filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.pattern.as_ref().is_match(d.product()))
{
return false;
}
}
true
});
Ok(())
})?;
if s9pk.is_empty() {
Ok(None)
} else {
Ok(Some(s9pk))
if ModelExt::as_value(self.as_s9pks())
.as_array()
.map_or(true, |s| s.is_empty())
{
return Ok(false);
}
}
Ok(true)
}
}

View File

@@ -242,12 +242,13 @@ impl TryFrom<ManifestV1> for Manifest {
.device
.into_iter()
.map(|(class, product)| DeviceFilter {
pattern_description: format!(
description: format!(
"a {class} device matching the expression {}",
product.as_ref()
),
class,
pattern: product,
product: Some(product),
..Default::default()
})
.collect(),
},

View File

@@ -15,6 +15,7 @@ use crate::s9pk::git_hash::GitHash;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex;
use crate::util::{VersionString, mime};
use crate::version::{Current, VersionT};
@@ -189,21 +190,107 @@ impl HardwareRequirements {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DeviceFilter {
pub description: String,
#[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString,
#[ts(type = "string")]
pub pattern: Regex,
pub pattern_description: String,
#[ts(type = "string | null")]
pub product: Option<Regex>,
#[ts(type = "string | null")]
pub vendor: Option<Regex>,
#[ts(optional)]
pub capabilities: Option<BTreeSet<InternedString>>,
#[ts(optional)]
pub driver: Option<InternedString>,
}
// Omit description
impl PartialEq for DeviceFilter {
fn eq(&self, other: &Self) -> bool {
self.class == other.class
&& InternedString::from_display(self.pattern.as_ref())
== InternedString::from_display(other.pattern.as_ref())
&& self.product == other.product
&& self.vendor == other.vendor
&& self.capabilities == other.capabilities
&& self.driver == other.driver
}
}
impl DeviceFilter {
pub fn matches(&self, device: &LshwDevice) -> bool {
if &*self.class != device.class() {
return false;
}
match device {
LshwDevice::Processor(LshwProcessor {
product,
vendor,
capabilities,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
true
}
LshwDevice::Display(LshwDisplay {
product,
vendor,
capabilities,
driver,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
if !self
.driver
.as_ref()
.map_or(true, |d| Some(d) == driver.as_ref())
{
return false;
}
true
}
}
}
}

View File

@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use clap::Parser;
use imbl::OrdMap;
use imbl_value::Value;
use once_cell::sync::OnceCell;
use rpc_toolkit::yajrc::RpcError;
@@ -53,7 +54,13 @@ impl Context for ContainerCliContext {
}
impl CallRemote<EffectContext> for ContainerCliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
call_remote_socket(
tokio::net::UnixStream::connect(&self.0.socket)
.await

View File

@@ -9,7 +9,6 @@ use std::sync::{Arc, Weak};
use std::time::Duration;
use axum::extract::ws::Utf8Bytes;
use crate::util::net::WebSocket;
use clap::Parser;
use futures::future::BoxFuture;
use futures::stream::FusedStream;
@@ -48,6 +47,7 @@ use crate::util::Never;
use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::future::NonDetachingJoinHandle;
use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
use crate::util::net::WebSocket;
use crate::util::serde::Pem;
use crate::util::sync::SyncMutex;
use crate::volume::data_dir;

View File

@@ -51,6 +51,7 @@ impl Model<StatusInfo> {
}
pub fn stopped(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?;
self.as_health_mut().ser(&Default::default())?;
Ok(())
}
pub fn restart(&mut self) -> Result<(), Error> {
@@ -59,7 +60,7 @@ impl Model<StatusInfo> {
Ok(())
}
pub fn init(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?;
self.stopped()?;
self.as_desired_mut().map_mutate(|s| {
Ok(match s {
DesiredStatus::BackingUp {

View File

@@ -251,6 +251,8 @@ impl CallRemote<TunnelContext> for CliContext {
async fn call_remote(
&self,
mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
@@ -315,6 +317,7 @@ impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
async fn call_remote(
&self,
mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
TunnelUrlParams { tunnel }: TunnelUrlParams,
) -> Result<Value, RpcError> {

View File

@@ -1,9 +1,12 @@
use std::collections::BTreeSet;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use crate::prelude::*;
use crate::util::Invoke;
use crate::{Error, ResultExt};
const KNOWN_CLASSES: &[&str] = &["processor", "display"];
@@ -22,22 +25,57 @@ impl LshwDevice {
Self::Display(_) => "display",
}
}
pub fn product(&self) -> &str {
match self {
Self::Processor(hw) => hw.product.as_str(),
Self::Display(hw) => hw.product.as_str(),
pub fn from_value(value: &Value) -> Option<Self> {
match value["class"].as_str() {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
Some("display") => Some(LshwDevice::Display(LshwDisplay::from_value(value))),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwProcessor {
pub product: String,
pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
}
impl LshwProcessor {
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwDisplay {
pub product: String,
pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
pub driver: Option<InternedString>,
}
impl LshwDisplay {
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
driver: value["configuration"]["driver"].as_str().map(From::from),
}
}
}
pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
@@ -47,19 +85,10 @@ pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
cmd.arg("-class").arg(*class);
}
Ok(
serde_json::from_slice::<Vec<serde_json::Value>>(
&cmd.invoke(crate::ErrorKind::Lshw).await?,
)
.with_kind(crate::ErrorKind::Deserialization)?
.into_iter()
.filter_map(|v| match serde_json::from_value(v) {
Ok(a) => Some(a),
Err(e) => {
tracing::error!("Failed to parse lshw output: {e}");
tracing::debug!("{e:?}");
None
}
})
.collect(),
serde_json::from_slice::<Vec<Value>>(&cmd.invoke(crate::ErrorKind::Lshw).await?)
.with_kind(crate::ErrorKind::Deserialization)?
.iter()
.filter_map(LshwDevice::from_value)
.collect(),
)
}

View File

@@ -1127,6 +1127,11 @@ impl Serialize for Regex {
serialize_display(&self.0, serializer)
}
}
impl PartialEq for Regex {
fn eq(&self, other: &Self) -> bool {
InternedString::from_display(self.as_ref()) == InternedString::from_display(other.as_ref())
}
}
// TODO: make this not allocate
#[derive(Debug)]

View File

@@ -1,6 +1,7 @@
use std::path::Path;
use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use tokio::fs::File;
use super::v0_3_5::V0_3_0_COMPAT;
@@ -10,7 +11,7 @@ use crate::context::RpcContext;
use crate::install::PKG_ARCHIVE_DIR;
use crate::prelude::*;
use crate::s9pk::S9pk;
use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::MerkleArchive;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::v2::SIG_CONTEXT;
@@ -84,28 +85,8 @@ impl VersionT for Version {
let mut manifest = previous_manifest.clone();
if let Some(device) =
previous_manifest["hardwareRequirements"]["device"].as_object()
{
manifest["hardwareRequirements"]["device"] = to_value(
&device
.into_iter()
.map(|(class, product)| {
Ok::<_, Error>(DeviceFilter {
pattern_description: format!(
"a {class} device matching the expression {}",
&product
),
class: class.clone(),
pattern: from_value(product.clone())?,
})
})
.fold(Ok::<_, Error>(Vec::new()), |acc, value| {
let mut acc = acc?;
acc.push(value?);
Ok(acc)
})?,
)?;
if let Some(_) = previous_manifest["hardwareRequirements"]["device"].as_object() {
manifest["hardwareRequirements"]["device"] = json!([]);
}
if previous_manifest != manifest {

View File

@@ -29,34 +29,6 @@ if [ -f /etc/default/grub ]; then
else
echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' >> /etc/default/grub
fi
cat > /etc/grub.d/01_reboot_efi <<-EOF
#!/bin/sh
set -e
# Only affect EFI systems
if [ ! -d /sys/firmware/efi ]; then
exit 0
fi
# Import helpers (path is Debian/Ubuntu style)
. /usr/lib/grub/grub-mkconfig_lib
# Append reboot=efi to GRUB_CMDLINE_LINUX* seen by later scripts
if [ -n "\${GRUB_CMDLINE_LINUX}" ]; then
GRUB_CMDLINE_LINUX="\${GRUB_CMDLINE_LINUX} reboot=efi"
else
GRUB_CMDLINE_LINUX="reboot=efi"
fi
if [ -n "\${GRUB_CMDLINE_LINUX_DEFAULT}" ]; then
GRUB_CMDLINE_LINUX_DEFAULT="\${GRUB_CMDLINE_LINUX_DEFAULT} reboot=efi"
fi
export GRUB_CMDLINE_LINUX GRUB_CMDLINE_LINUX_DEFAULT
EOF
chmod +x /etc/grub.d/01_reboot_efi
fi
VERSION="$(cat /usr/lib/startos/VERSION.txt)"

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddMirrorParams = {
url: string
commitment: MerkleArchiveCommitment
signature: AnySignature
}

View File

@@ -3,7 +3,7 @@ import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddPackageParams = {
url: string
urls: string[]
commitment: MerkleArchiveCommitment
signature: AnySignature
}

View File

@@ -1,7 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DeviceFilter = {
description: string
class: "processor" | "display"
pattern: string
patternDescription: string
product: string | null
vendor: string | null
capabilities?: Array<string>
driver?: string
}

View File

@@ -7,5 +7,5 @@ export type GetPackageParams = {
id: PackageId | null
targetVersion: string | null
sourceVersion: Version | null
otherVersions: PackageDetailLevel
otherVersions: PackageDetailLevel | null
}

View File

@@ -1,3 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwDisplay = { product: string }
export type LshwDisplay = {
product: string | null
vendor: string | null
capabilities: Array<string>
driver: string | null
}

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwProcessor = { product: string }
export type LshwProcessor = {
product: string | null
vendor: string | null
capabilities: Array<string>
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
export type RemoveMirrorParams = {
id: PackageId
version: Version
url: string
}

View File

@@ -1,5 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Base64 } from "./Base64"
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
export type RemovePackageParams = { id: PackageId; version: Version }
export type RemovePackageParams = {
id: PackageId
version: Version
sighash: Base64 | null
}

View File

@@ -13,6 +13,7 @@ export { ActionVisibility } from "./ActionVisibility"
export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams"
export { AddCategoryParams } from "./AddCategoryParams"
export { AddMirrorParams } from "./AddMirrorParams"
export { AddPackageParams } from "./AddPackageParams"
export { AddPackageSignerParams } from "./AddPackageSignerParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
@@ -171,6 +172,7 @@ export { RegistryInfo } from "./RegistryInfo"
export { RemoveAdminParams } from "./RemoveAdminParams"
export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemoveMirrorParams } from "./RemoveMirrorParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams"
export { RemovePackageSignerParams } from "./RemovePackageSignerParams"